Stardew Valley: Refactor buildings to use content packs (#4239)

* create building data object and rename ItemSource to Source to be more generic

# Conflicts:
#	worlds/stardew_valley/content/game_content.py

# Conflicts:
#	worlds/stardew_valley/data/artisan.py
#	worlds/stardew_valley/data/game_item.py
#	worlds/stardew_valley/data/harvest.py
#	worlds/stardew_valley/data/shop.py

* remove compound sources, replace by other requirements which already handle this usecase

* add coops to content packs

* add building progression in game features

* add shippping bin to starting building; remove has_house

* replace config check with feature

* add other buildings in content packs

* not passing

* tests passes, unbelievable

* use newly create methods more

* use new assets to ease readability

* self review

* fix flake8 maybe

* properly split rule for mapping cave systems

* fix tractor garage name

* self review

* add upgrade_from to farm house buldings

* don't override building name variable in logic

* remove has_group from buildings

* mark some items easy in grinding logic so blueprints buildings can be in more early spheres

* move stuff around to maybe avoid future conflicts cuz I have like 10 PRs opened right now

* remove price_multiplier, turns out it's unused during generation

* disable shop source for mapping cave systems

* bunch of code review changes

* add petbowl and farmhouse to autobuilding

* set min easy items to 300

* fix farm type
This commit is contained in:
Jérémie Bolduc
2025-04-08 12:37:45 -04:00
committed by GitHub
parent 286e24629f
commit 9ac921380f
56 changed files with 757 additions and 460 deletions

View File

@@ -145,7 +145,7 @@ class StardewValleyWorld(World):
def create_items(self):
self.precollect_starting_season()
self.precollect_farm_type_items()
self.precollect_building_items()
items_to_exclude = [excluded_items
for excluded_items in self.multiworld.precollected_items[self.player]
if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK,
@@ -200,9 +200,16 @@ class StardewValleyWorld(World):
starting_season = self.create_item(self.random.choice(season_pool))
self.multiworld.push_precollected(starting_season)
def precollect_farm_type_items(self):
if self.options.farm_type == FarmType.option_meadowlands and self.options.building_progression & BuildingProgression.option_progressive:
self.multiworld.push_precollected(self.create_item("Progressive Coop"))
def precollect_building_items(self):
building_progression = self.content.features.building_progression
# Not adding items when building are vanilla because the buildings are already placed in the world.
if not building_progression.is_progressive:
return
for building in building_progression.starting_buildings:
item, quantity = building_progression.to_progressive_item(building)
for _ in range(quantity):
self.multiworld.push_precollected(self.create_item(item))
def setup_logic_events(self):
def register_event(name: str, region: str, rule: StardewRule):

View File

@@ -1,8 +1,9 @@
from . import content_packs
from .feature import cropsanity, friendsanity, fishsanity, booksanity, skill_progression, tool_progression
from .feature import cropsanity, friendsanity, fishsanity, booksanity, building_progression, skill_progression, tool_progression
from .game_content import ContentPack, StardewContent, StardewFeatures
from .unpacking import unpack_content
from .. import options
from ..strings.building_names import Building
def create_content(player_options: options.StardewValleyOptions) -> StardewContent:
@@ -29,6 +30,7 @@ def choose_content_packs(player_options: options.StardewValleyOptions):
def choose_features(player_options: options.StardewValleyOptions) -> StardewFeatures:
return StardewFeatures(
choose_booksanity(player_options.booksanity),
choose_building_progression(player_options.building_progression, player_options.farm_type),
choose_cropsanity(player_options.cropsanity),
choose_fishsanity(player_options.fishsanity),
choose_friendsanity(player_options.friendsanity, player_options.friendsanity_heart_size),
@@ -109,6 +111,32 @@ def choose_friendsanity(friendsanity_option: options.Friendsanity, heart_size: o
raise ValueError(f"No friendsanity feature mapped to {str(friendsanity_option.value)}")
def choose_building_progression(building_option: options.BuildingProgression,
farm_type_option: options.FarmType) -> building_progression.BuildingProgressionFeature:
starting_buildings = {Building.farm_house, Building.pet_bowl, Building.shipping_bin}
if farm_type_option == options.FarmType.option_meadowlands:
starting_buildings.add(Building.coop)
if (building_option == options.BuildingProgression.option_vanilla
or building_option == options.BuildingProgression.option_vanilla_cheap
or building_option == options.BuildingProgression.option_vanilla_very_cheap):
return building_progression.BuildingProgressionVanilla(
starting_buildings=starting_buildings,
)
starting_buildings.remove(Building.shipping_bin)
if (building_option == options.BuildingProgression.option_progressive
or building_option == options.BuildingProgression.option_progressive_cheap
or building_option == options.BuildingProgression.option_progressive_very_cheap):
return building_progression.BuildingProgressionProgressive(
starting_buildings=starting_buildings,
)
raise ValueError(f"No building progression feature mapped to {str(building_option.value)}")
skill_progression_by_option = {
options.SkillProgression.option_vanilla: skill_progression.SkillProgressionVanilla(),
options.SkillProgression.option_progressive: skill_progression.SkillProgressionProgressive(),

View File

@@ -1,4 +1,5 @@
from . import booksanity
from . import building_progression
from . import cropsanity
from . import fishsanity
from . import friendsanity

View File

@@ -0,0 +1,53 @@
from abc import ABC
from dataclasses import dataclass
from typing import ClassVar, Set, Tuple
from ...strings.building_names import Building
progressive_house = "Progressive House"
# This assumes that the farm house is always available, which might not be true forever...
progressive_house_by_upgrade_name = {
Building.farm_house: 0,
Building.kitchen: 1,
Building.kids_room: 2,
Building.cellar: 3
}
def to_progressive_item(building: str) -> Tuple[str, int]:
"""Return the name of the progressive item and its quantity required to unlock the building.
"""
if building in [Building.coop, Building.barn, Building.shed]:
return f"Progressive {building}", 1
elif building.startswith("Big"):
return f"Progressive {building[building.index(' ') + 1:]}", 2
elif building.startswith("Deluxe"):
return f"Progressive {building[building.index(' ') + 1:]}", 3
elif building in progressive_house_by_upgrade_name:
return progressive_house, progressive_house_by_upgrade_name[building]
return building, 1
def to_location_name(building: str) -> str:
return f"{building} Blueprint"
@dataclass(frozen=True)
class BuildingProgressionFeature(ABC):
is_progressive: ClassVar[bool]
starting_buildings: Set[str]
to_progressive_item = staticmethod(to_progressive_item)
progressive_house = progressive_house
to_location_name = staticmethod(to_location_name)
class BuildingProgressionVanilla(BuildingProgressionFeature):
is_progressive = False
class BuildingProgressionProgressive(BuildingProgressionFeature):
is_progressive = True

View File

@@ -3,9 +3,10 @@ from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, Iterable, Set, Any, Mapping, Type, Tuple, Union
from .feature import booksanity, cropsanity, fishsanity, friendsanity, skill_progression, tool_progression
from .feature import booksanity, cropsanity, fishsanity, friendsanity, skill_progression, building_progression, tool_progression
from ..data.building import Building
from ..data.fish_data import FishItem
from ..data.game_item import GameItem, ItemSource, ItemTag
from ..data.game_item import GameItem, Source, ItemTag
from ..data.skill import Skill
from ..data.villagers_data import Villager
@@ -20,16 +21,17 @@ class StardewContent:
game_items: Dict[str, GameItem] = field(default_factory=dict)
fishes: Dict[str, FishItem] = field(default_factory=dict)
villagers: Dict[str, Villager] = field(default_factory=dict)
farm_buildings: Dict[str, Building] = field(default_factory=dict)
skills: Dict[str, Skill] = field(default_factory=dict)
quests: Dict[str, Any] = field(default_factory=dict)
def find_sources_of_type(self, types: Union[Type[ItemSource], Tuple[Type[ItemSource]]]) -> Iterable[ItemSource]:
def find_sources_of_type(self, types: Union[Type[Source], Tuple[Type[Source]]]) -> Iterable[Source]:
for item in self.game_items.values():
for source in item.sources:
if isinstance(source, types):
yield source
def source_item(self, item_name: str, *sources: ItemSource):
def source_item(self, item_name: str, *sources: Source):
item = self.game_items.setdefault(item_name, GameItem(item_name))
item.add_sources(sources)
@@ -50,6 +52,7 @@ class StardewContent:
@dataclass(frozen=True)
class StardewFeatures:
booksanity: booksanity.BooksanityFeature
building_progression: building_progression.BuildingProgressionFeature
cropsanity: cropsanity.CropsanityFeature
fishsanity: fishsanity.FishsanityFeature
friendsanity: friendsanity.FriendsanityFeature
@@ -70,13 +73,13 @@ class ContentPack:
# def item_hook
# ...
harvest_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict)
harvest_sources: Mapping[str, Iterable[Source]] = field(default_factory=dict)
"""Harvest sources contains both crops and forageables, but also fruits from trees, the cave farm and stuff harvested from tapping like maple syrup."""
def harvest_source_hook(self, content: StardewContent):
...
shop_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict)
shop_sources: Mapping[str, Iterable[Source]] = field(default_factory=dict)
def shop_source_hook(self, content: StardewContent):
...
@@ -86,12 +89,12 @@ class ContentPack:
def fish_hook(self, content: StardewContent):
...
crafting_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict)
crafting_sources: Mapping[str, Iterable[Source]] = field(default_factory=dict)
def crafting_hook(self, content: StardewContent):
...
artisan_good_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict)
artisan_good_sources: Mapping[str, Iterable[Source]] = field(default_factory=dict)
def artisan_good_hook(self, content: StardewContent):
...
@@ -101,6 +104,11 @@ class ContentPack:
def villager_hook(self, content: StardewContent):
...
farm_buildings: Iterable[Building] = ()
def farm_building_hook(self, content: StardewContent):
...
skills: Iterable[Skill] = ()
def skill_hook(self, content: StardewContent):

View File

@@ -1,7 +1,25 @@
from ..game_content import ContentPack
from ..mod_registry import register_mod_content_pack
from ...data.building import Building
from ...data.shop import ShopSource
from ...mods.mod_data import ModNames
from ...strings.artisan_good_names import ArtisanGood
from ...strings.building_names import ModBuilding
from ...strings.metal_names import MetalBar
from ...strings.region_names import Region
register_mod_content_pack(ContentPack(
ModNames.tractor,
farm_buildings=(
Building(
ModBuilding.tractor_garage,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=150_000,
items_price=((20, MetalBar.iron), (5, MetalBar.iridium), (1, ArtisanGood.battery_pack)),
),
),
),
),
))

View File

@@ -5,7 +5,7 @@ from typing import Iterable, Mapping, Callable
from .game_content import StardewContent, ContentPack, StardewFeatures
from .vanilla.base import base_game as base_game_content_pack
from ..data.game_item import GameItem, ItemSource
from ..data.game_item import GameItem, Source
def unpack_content(features: StardewFeatures, packs: Iterable[ContentPack]) -> StardewContent:
@@ -61,6 +61,10 @@ def register_pack(content: StardewContent, pack: ContentPack):
content.villagers[villager.name] = villager
pack.villager_hook(content)
for building in pack.farm_buildings:
content.farm_buildings[building.name] = building
pack.farm_building_hook(content)
for skill in pack.skills:
content.skills[skill.name] = skill
pack.skill_hook(content)
@@ -73,7 +77,7 @@ def register_pack(content: StardewContent, pack: ContentPack):
def register_sources_and_call_hook(content: StardewContent,
sources_by_item_name: Mapping[str, Iterable[ItemSource]],
sources_by_item_name: Mapping[str, Iterable[Source]],
hook: Callable[[StardewContent], None]):
for item_name, sources in sources_by_item_name.items():
item = content.game_items.setdefault(item_name, GameItem(item_name))

View File

@@ -1,10 +1,13 @@
from ..game_content import ContentPack
from ...data import villagers_data, fish_data
from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource, CompoundSource
from ...data.building import Building
from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource
from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource
from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement
from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
from ...strings.artisan_good_names import ArtisanGood
from ...strings.book_names import Book
from ...strings.building_names import Building as BuildingNames
from ...strings.crop_names import Fruit
from ...strings.fish_names import WaterItem
from ...strings.food_names import Beverage, Meal
@@ -12,6 +15,7 @@ from ...strings.forageable_names import Forageable, Mushroom
from ...strings.fruit_tree_names import Sapling
from ...strings.generic_names import Generic
from ...strings.material_names import Material
from ...strings.metal_names import MetalBar
from ...strings.region_names import Region, LogicRegion
from ...strings.season_names import Season
from ...strings.seed_names import Seed, TreeSeed
@@ -229,10 +233,10 @@ pelican_town = ContentPack(
ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
Book.mapping_cave_systems: (
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
CompoundSource(sources=(
GenericSource(regions=(Region.adventurer_guild_bedroom,)),
ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),
))),
# Disabling the shop source for better game design.
# ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),
),
Book.monster_compendium: (
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
CustomRuleSource(create_rule=lambda logic: logic.monster.can_kill_many(Generic.any)),
@@ -385,5 +389,204 @@ pelican_town = ContentPack(
villagers_data.vincent,
villagers_data.willy,
villagers_data.wizard,
),
farm_buildings=(
Building(
BuildingNames.barn,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=6000,
items_price=((350, Material.wood), (150, Material.stone))
),
),
),
Building(
BuildingNames.big_barn,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=12_000,
items_price=((450, Material.wood), (200, Material.stone))
),
),
upgrade_from=BuildingNames.barn,
),
Building(
BuildingNames.deluxe_barn,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=25_000,
items_price=((550, Material.wood), (300, Material.stone))
),
),
upgrade_from=BuildingNames.big_barn,
),
Building(
BuildingNames.coop,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=4000,
items_price=((300, Material.wood), (100, Material.stone))
),
),
),
Building(
BuildingNames.big_coop,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=10_000,
items_price=((400, Material.wood), (150, Material.stone))
),
),
upgrade_from=BuildingNames.coop,
),
Building(
BuildingNames.deluxe_coop,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=20_000,
items_price=((500, Material.wood), (200, Material.stone))
),
),
upgrade_from=BuildingNames.big_coop,
),
Building(
BuildingNames.fish_pond,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=5000,
items_price=((200, Material.stone), (5, WaterItem.seaweed), (5, WaterItem.green_algae))
),
),
),
Building(
BuildingNames.mill,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=2500,
items_price=((50, Material.stone), (150, Material.wood), (4, ArtisanGood.cloth))
),
),
),
Building(
BuildingNames.shed,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=15_000,
items_price=((300, Material.wood),)
),
),
),
Building(
BuildingNames.big_shed,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=20_000,
items_price=((550, Material.wood), (300, Material.stone))
),
),
upgrade_from=BuildingNames.shed,
),
Building(
BuildingNames.silo,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=100,
items_price=((100, Material.stone), (10, Material.clay), (5, MetalBar.copper))
),
),
),
Building(
BuildingNames.slime_hutch,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=10_000,
items_price=((500, Material.stone), (10, MetalBar.quartz), (1, MetalBar.iridium))
),
),
),
Building(
BuildingNames.stable,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=10_000,
items_price=((100, Material.hardwood), (5, MetalBar.iron))
),
),
),
Building(
BuildingNames.well,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=1000,
items_price=((75, Material.stone),)
),
),
),
Building(
BuildingNames.shipping_bin,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=250,
items_price=((150, Material.wood),)
),
),
),
Building(
BuildingNames.pet_bowl,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=5000,
items_price=((25, Material.hardwood),)
),
),
),
Building(
BuildingNames.kitchen,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=10_000,
items_price=((450, Material.wood),)
),
),
upgrade_from=BuildingNames.farm_house,
),
Building(
BuildingNames.kids_room,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=65_000,
items_price=((100, Material.hardwood),)
),
),
upgrade_from=BuildingNames.kitchen,
),
Building(
BuildingNames.cellar,
sources=(
ShopSource(
shop_region=Region.carpenter,
money_price=100_000,
),
),
upgrade_from=BuildingNames.kids_room,
),
)
)

View File

@@ -1,10 +1,10 @@
from dataclasses import dataclass
from .game_item import ItemSource
from .game_item import Source
@dataclass(frozen=True, kw_only=True)
class MachineSource(ItemSource):
class MachineSource(Source):
item: str # this should be optional (worm bin)
machine: str
# seasons

View File

@@ -0,0 +1,16 @@
from dataclasses import dataclass, field
from functools import cached_property
from typing import Optional, Tuple
from .game_item import Source
@dataclass(frozen=True)
class Building:
name: str
sources: Tuple[Source, ...] = field(kw_only=True)
upgrade_from: Optional[str] = field(default=None, kw_only=True)
@cached_property
def is_upgrade(self) -> bool:
return self.upgrade_from is not None

View File

@@ -27,7 +27,7 @@ class ItemTag(enum.Enum):
@dataclass(frozen=True)
class ItemSource(ABC):
class Source(ABC):
add_tags: ClassVar[Tuple[ItemTag]] = ()
other_requirements: Tuple[Requirement, ...] = field(kw_only=True, default_factory=tuple)
@@ -38,23 +38,18 @@ class ItemSource(ABC):
@dataclass(frozen=True, kw_only=True)
class GenericSource(ItemSource):
class GenericSource(Source):
regions: Tuple[str, ...] = ()
"""No region means it's available everywhere."""
@dataclass(frozen=True)
class CustomRuleSource(ItemSource):
class CustomRuleSource(Source):
"""Hopefully once everything is migrated to sources, we won't need these custom logic anymore."""
create_rule: Callable[[Any], StardewRule]
@dataclass(frozen=True, kw_only=True)
class CompoundSource(ItemSource):
sources: Tuple[ItemSource, ...] = ()
class Tag(ItemSource):
class Tag(Source):
"""Not a real source, just a way to add tags to an item. Will be removed from the item sources during unpacking."""
tag: Tuple[ItemTag, ...]
@@ -69,10 +64,10 @@ class Tag(ItemSource):
@dataclass(frozen=True)
class GameItem:
name: str
sources: List[ItemSource] = field(default_factory=list)
sources: List[Source] = field(default_factory=list)
tags: Set[ItemTag] = field(default_factory=set)
def add_sources(self, sources: Iterable[ItemSource]):
def add_sources(self, sources: Iterable[Source]):
self.sources.extend(source for source in sources if type(source) is not Tag)
for source in sources:
self.add_tags(source.add_tags)

View File

@@ -1,18 +1,18 @@
from dataclasses import dataclass
from typing import Tuple, Sequence, Mapping
from .game_item import ItemSource, ItemTag
from .game_item import Source, ItemTag
from ..strings.season_names import Season
@dataclass(frozen=True, kw_only=True)
class ForagingSource(ItemSource):
class ForagingSource(Source):
regions: Tuple[str, ...]
seasons: Tuple[str, ...] = Season.all
@dataclass(frozen=True, kw_only=True)
class SeasonalForagingSource(ItemSource):
class SeasonalForagingSource(Source):
season: str
days: Sequence[int]
regions: Tuple[str, ...]
@@ -22,17 +22,17 @@ class SeasonalForagingSource(ItemSource):
@dataclass(frozen=True, kw_only=True)
class FruitBatsSource(ItemSource):
class FruitBatsSource(Source):
...
@dataclass(frozen=True, kw_only=True)
class MushroomCaveSource(ItemSource):
class MushroomCaveSource(Source):
...
@dataclass(frozen=True, kw_only=True)
class HarvestFruitTreeSource(ItemSource):
class HarvestFruitTreeSource(Source):
add_tags = (ItemTag.CROPSANITY,)
sapling: str
@@ -46,7 +46,7 @@ class HarvestFruitTreeSource(ItemSource):
@dataclass(frozen=True, kw_only=True)
class HarvestCropSource(ItemSource):
class HarvestCropSource(Source):
add_tags = (ItemTag.CROPSANITY,)
seed: str
@@ -61,5 +61,5 @@ class HarvestCropSource(ItemSource):
@dataclass(frozen=True, kw_only=True)
class ArtifactSpotSource(ItemSource):
class ArtifactSpotSource(Source):
amount: int

View File

@@ -509,6 +509,7 @@ id,name,classification,groups,mod_name
561,Fishing Bar Size Bonus,filler,PLAYER_BUFF,
562,Quality Bonus,filler,PLAYER_BUFF,
563,Glow Bonus,filler,PLAYER_BUFF,
564,Pet Bowl,progression,BUILDING,
4001,Burnt Trap,trap,TRAP,
4002,Darkness Trap,trap,TRAP,
4003,Frozen Trap,trap,TRAP,
1 id name classification groups mod_name
509 561 Fishing Bar Size Bonus filler PLAYER_BUFF
510 562 Quality Bonus filler PLAYER_BUFF
511 563 Glow Bonus filler PLAYER_BUFF
512 564 Pet Bowl progression BUILDING
513 4001 Burnt Trap trap TRAP
514 4002 Darkness Trap trap TRAP
515 4003 Frozen Trap trap TRAP

View File

@@ -21,6 +21,11 @@ class SkillRequirement(Requirement):
level: int
@dataclass(frozen=True)
class RegionRequirement(Requirement):
region: str
@dataclass(frozen=True)
class SeasonRequirement(Requirement):
season: str

View File

@@ -1,14 +1,14 @@
from dataclasses import dataclass
from typing import Tuple, Optional
from .game_item import ItemSource
from .game_item import Source
from ..strings.season_names import Season
ItemPrice = Tuple[int, str]
@dataclass(frozen=True, kw_only=True)
class ShopSource(ItemSource):
class ShopSource(Source):
shop_region: str
money_price: Optional[int] = None
items_price: Optional[Tuple[ItemPrice, ...]] = None
@@ -20,20 +20,20 @@ class ShopSource(ItemSource):
@dataclass(frozen=True, kw_only=True)
class MysteryBoxSource(ItemSource):
class MysteryBoxSource(Source):
amount: int
@dataclass(frozen=True, kw_only=True)
class ArtifactTroveSource(ItemSource):
class ArtifactTroveSource(Source):
amount: int
@dataclass(frozen=True, kw_only=True)
class PrizeMachineSource(ItemSource):
class PrizeMachineSource(Source):
amount: int
@dataclass(frozen=True, kw_only=True)
class FishingTreasureChestSource(ItemSource):
class FishingTreasureChestSource(Source):
amount: int

View File

@@ -23,9 +23,9 @@ def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions,
add_seasonal_candidates(early_candidates, options)
if options.building_progression & stardew_options.BuildingProgression.option_progressive:
if content.features.building_progression.is_progressive:
early_forced.append(Building.shipping_bin)
if options.farm_type != stardew_options.FarmType.option_meadowlands:
if Building.coop not in content.features.building_progression.starting_buildings:
early_candidates.append("Progressive Coop")
early_candidates.append("Progressive Barn")

View File

@@ -15,7 +15,7 @@ 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, \
BuildingProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \
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
@@ -225,7 +225,7 @@ def create_unique_items(item_factory: StardewItemFactory, options: StardewValley
create_tools(item_factory, content, items)
create_skills(item_factory, content, items)
create_wizard_buildings(item_factory, options, items)
create_carpenter_buildings(item_factory, options, items)
create_carpenter_buildings(item_factory, content, items)
items.append(item_factory("Railroad Boulder Removed"))
items.append(item_factory(CommunityUpgrade.fruit_bats))
items.append(item_factory(CommunityUpgrade.mushroom_boxes))
@@ -353,30 +353,14 @@ def create_wizard_buildings(item_factory: StardewItemFactory, options: StardewVa
items.append(item_factory("Woods Obelisk"))
def create_carpenter_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
building_option = options.building_progression
if not building_option & BuildingProgression.option_progressive:
def create_carpenter_buildings(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]):
building_progression = content.features.building_progression
if not building_progression.is_progressive:
return
items.append(item_factory("Progressive Coop"))
items.append(item_factory("Progressive Coop"))
items.append(item_factory("Progressive Coop"))
items.append(item_factory("Progressive Barn"))
items.append(item_factory("Progressive Barn"))
items.append(item_factory("Progressive Barn"))
items.append(item_factory("Well"))
items.append(item_factory("Silo"))
items.append(item_factory("Mill"))
items.append(item_factory("Progressive Shed"))
items.append(item_factory("Progressive Shed", ItemClassification.useful))
items.append(item_factory("Fish Pond"))
items.append(item_factory("Stable"))
items.append(item_factory("Slime Hutch"))
items.append(item_factory("Shipping Bin"))
items.append(item_factory("Progressive House"))
items.append(item_factory("Progressive House"))
items.append(item_factory("Progressive House"))
if ModNames.tractor in options.mods:
items.append(item_factory("Tractor Garage"))
for building in content.farm_buildings.values():
item_name, _ = building_progression.to_progressive_item(building.name)
items.append(item_factory(item_name))
def create_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):

View File

@@ -11,7 +11,7 @@ from .data.game_item import ItemTag
from .data.museum_data import all_museum_items
from .mods.mod_data import ModNames
from .options import ExcludeGingerIsland, ArcadeMachineLocations, SpecialOrderLocations, Museumsanity, \
FestivalLocations, BuildingProgression, ElevatorProgression, BackpackProgression, FarmType
FestivalLocations, ElevatorProgression, BackpackProgression, FarmType
from .options import StardewValleyOptions, Craftsanity, Chefsanity, Cooksanity, Shipsanity, Monstersanity
from .strings.goal_names import Goal
from .strings.quest_names import ModQuest, Quest
@@ -261,6 +261,19 @@ def extend_baby_locations(randomized_locations: List[LocationData]):
randomized_locations.extend(baby_locations)
def extend_building_locations(randomized_locations: List[LocationData], content: StardewContent):
building_progression = content.features.building_progression
if not building_progression.is_progressive:
return
for building in content.farm_buildings.values():
if building.name in building_progression.starting_buildings:
continue
location_name = building_progression.to_location_name(building.name)
randomized_locations.append(location_table[location_name])
def extend_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random):
if options.festival_locations == FestivalLocations.option_disabled:
return
@@ -485,10 +498,7 @@ def create_locations(location_collector: StardewLocationCollector,
if skill_progression.is_mastery_randomized(skill):
randomized_locations.append(location_table[skill.mastery_name])
if options.building_progression & BuildingProgression.option_progressive:
for location in locations_by_tag[LocationTags.BUILDING_BLUEPRINT]:
if location.mod_name is None or location.mod_name in options.mods:
randomized_locations.append(location_table[location.name])
extend_building_locations(randomized_locations, content)
if options.arcade_machine_locations != ArcadeMachineLocations.option_disabled:
randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE_VICTORY])

View File

@@ -20,7 +20,6 @@ class LogicRegistry:
self.museum_rules: Dict[str, StardewRule] = {}
self.festival_rules: Dict[str, StardewRule] = {}
self.quest_rules: Dict[str, StardewRule] = {}
self.building_rules: Dict[str, StardewRule] = {}
self.special_order_rules: Dict[str, StardewRule] = {}
self.sve_location_rules: Dict[str, StardewRule] = {}

View File

@@ -1,22 +1,22 @@
import typing
from functools import cached_property
from typing import Dict, Union
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogic, BaseLogicMixin
from .has_logic import HasLogicMixin
from .money_logic import MoneyLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from ..options import BuildingProgression
from ..stardew_rule import StardewRule, True_, False_, Has
from ..strings.artisan_good_names import ArtisanGood
from ..stardew_rule import StardewRule, true_
from ..strings.building_names import Building
from ..strings.fish_names import WaterItem
from ..strings.material_names import Material
from ..strings.metal_names import MetalBar
from ..strings.region_names import Region
has_group = "building"
if typing.TYPE_CHECKING:
from .source_logic import SourceLogicMixin
else:
SourceLogicMixin = object
AUTO_BUILDING_BUILDINGS = {Building.shipping_bin, Building.pet_bowl, Building.farm_house}
class BuildingLogicMixin(BaseLogicMixin):
@@ -25,78 +25,38 @@ class BuildingLogicMixin(BaseLogicMixin):
self.building = BuildingLogic(*args, **kwargs)
class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, MoneyLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin]]):
def initialize_rules(self):
self.registry.building_rules.update({
# @formatter:off
Building.barn: self.logic.money.can_spend(6000) & self.logic.has_all(Material.wood, Material.stone),
Building.big_barn: self.logic.money.can_spend(12000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.barn),
Building.deluxe_barn: self.logic.money.can_spend(25000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.big_barn),
Building.coop: self.logic.money.can_spend(4000) & self.logic.has_all(Material.wood, Material.stone),
Building.big_coop: self.logic.money.can_spend(10000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.coop),
Building.deluxe_coop: self.logic.money.can_spend(20000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.big_coop),
Building.fish_pond: self.logic.money.can_spend(5000) & self.logic.has_all(Material.stone, WaterItem.seaweed, WaterItem.green_algae),
Building.mill: self.logic.money.can_spend(2500) & self.logic.has_all(Material.stone, Material.wood, ArtisanGood.cloth),
Building.shed: self.logic.money.can_spend(15000) & self.logic.has(Material.wood),
Building.big_shed: self.logic.money.can_spend(20000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.shed),
Building.silo: self.logic.money.can_spend(100) & self.logic.has_all(Material.stone, Material.clay, MetalBar.copper),
Building.slime_hutch: self.logic.money.can_spend(10000) & self.logic.has_all(Material.stone, MetalBar.quartz, MetalBar.iridium),
Building.stable: self.logic.money.can_spend(10000) & self.logic.has_all(Material.hardwood, MetalBar.iron),
Building.well: self.logic.money.can_spend(1000) & self.logic.has(Material.stone),
Building.shipping_bin: self.logic.money.can_spend(250) & self.logic.has(Material.wood),
Building.kitchen: self.logic.money.can_spend(10000) & self.logic.has(Material.wood) & self.logic.building.has_house(0),
Building.kids_room: self.logic.money.can_spend(65000) & self.logic.has(Material.hardwood) & self.logic.building.has_house(1),
Building.cellar: self.logic.money.can_spend(100000) & self.logic.building.has_house(2),
# @formatter:on
})
def update_rules(self, new_rules: Dict[str, StardewRule]):
self.registry.building_rules.update(new_rules)
class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SourceLogicMixin]]):
@cache_self1
def has_building(self, building: str) -> StardewRule:
# Shipping bin is special. The mod auto-builds it when received, no need to go to Robin.
if building is Building.shipping_bin:
if not self.options.building_progression & BuildingProgression.option_progressive:
return True_()
return self.logic.received(building)
def can_build(self, building_name: str) -> StardewRule:
building = self.content.farm_buildings.get(building_name)
assert building is not None, f"Building {building_name} not found."
source_rule = self.logic.source.has_access_to_any(building.sources)
if not building.is_upgrade:
return source_rule
upgrade_rule = self.logic.building.has_building(building.upgrade_from)
return self.logic.and_(upgrade_rule, source_rule)
@cache_self1
def has_building(self, building_name: str) -> StardewRule:
building_progression = self.content.features.building_progression
if building_name in building_progression.starting_buildings:
return true_
if not building_progression.is_progressive:
return self.logic.building.can_build(building_name)
# Those buildings are special. The mod auto-builds them when received, no need to go to Robin.
if building_name in AUTO_BUILDING_BUILDINGS:
return self.logic.received(Building.shipping_bin)
carpenter_rule = self.logic.building.can_construct_buildings
if not self.options.building_progression & BuildingProgression.option_progressive:
return Has(building, self.registry.building_rules, has_group) & carpenter_rule
count = 1
if building in [Building.coop, Building.barn, Building.shed]:
building = f"Progressive {building}"
elif building.startswith("Big"):
count = 2
building = " ".join(["Progressive", *building.split(" ")[1:]])
elif building.startswith("Deluxe"):
count = 3
building = " ".join(["Progressive", *building.split(" ")[1:]])
return self.logic.received(building, count) & carpenter_rule
item, count = building_progression.to_progressive_item(building_name)
return self.logic.received(item, count) & carpenter_rule
@cached_property
def can_construct_buildings(self) -> StardewRule:
return self.logic.region.can_reach(Region.carpenter)
@cache_self1
def has_house(self, upgrade_level: int) -> StardewRule:
if upgrade_level < 1:
return True_()
if upgrade_level > 3:
return False_()
carpenter_rule = self.logic.building.can_construct_buildings
if self.options.building_progression & BuildingProgression.option_progressive:
return carpenter_rule & self.logic.received(f"Progressive House", upgrade_level)
if upgrade_level == 1:
return carpenter_rule & Has(Building.kitchen, self.registry.building_rules, has_group)
if upgrade_level == 2:
return carpenter_rule & Has(Building.kids_room, self.registry.building_rules, has_group)
# if upgrade_level == 3:
return carpenter_rule & Has(Building.cellar, self.registry.building_rules, has_group)

View File

@@ -17,6 +17,7 @@ from ..data.recipe_data import RecipeSource, StarterSource, ShopSource, SkillSou
from ..data.recipe_source import CutsceneSource, ShopTradeSource
from ..options import Chefsanity
from ..stardew_rule import StardewRule, True_, False_
from ..strings.building_names import Building
from ..strings.region_names import LogicRegion
from ..strings.skill_names import Skill
from ..strings.tv_channel_names import Channel
@@ -32,7 +33,7 @@ class CookingLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogi
BuildingLogicMixin, RelationshipLogicMixin, SkillLogicMixin, CookingLogicMixin]]):
@cached_property
def can_cook_in_kitchen(self) -> StardewRule:
return self.logic.building.has_house(1) | self.logic.skill.has_level(Skill.foraging, 9)
return self.logic.building.has_building(Building.kitchen) | self.logic.skill.has_level(Skill.foraging, 9)
# Should be cached
def can_cook(self, recipe: CookingRecipe = None) -> StardewRule:

View File

@@ -44,7 +44,7 @@ class GoalLogic(BaseLogic[StardewLogic]):
self.logic.museum.can_complete_museum(),
# Catching every fish not expected
# Shipping every item not expected
self.logic.relationship.can_get_married() & self.logic.building.has_house(2),
self.logic.relationship.can_get_married() & self.logic.building.has_building(Building.kids_room),
self.logic.relationship.has_hearts_with_n(5, 8), # 5 Friends
self.logic.relationship.has_hearts_with_n(10, 8), # 10 friends
self.logic.pet.has_pet_hearts(5), # Max Pet

View File

@@ -13,6 +13,7 @@ from ..strings.craftable_names import Consumable
from ..strings.currency_names import Currency
from ..strings.fish_names import WaterChest
from ..strings.geode_names import Geode
from ..strings.material_names import Material
from ..strings.region_names import Region
from ..strings.tool_names import Tool
@@ -21,9 +22,14 @@ if TYPE_CHECKING:
else:
ToolLogicMixin = object
MIN_ITEMS = 10
MAX_ITEMS = 999
PERCENT_REQUIRED_FOR_MAX_ITEM = 24
MIN_MEDIUM_ITEMS = 10
MAX_MEDIUM_ITEMS = 999
PERCENT_REQUIRED_FOR_MAX_MEDIUM_ITEM = 24
EASY_ITEMS = {Material.wood, Material.stone, Material.fiber, Material.sap}
MIN_EASY_ITEMS = 300
MAX_EASY_ITEMS = 2997
PERCENT_REQUIRED_FOR_MAX_EASY_ITEM = 6
class GrindLogicMixin(BaseLogicMixin):
@@ -43,7 +49,7 @@ class GrindLogic(BaseLogic[Union[GrindLogicMixin, HasLogicMixin, ReceivedLogicMi
# Assuming one box per day, but halved because we don't know how many months have passed before Mr. Qi's Plane Ride.
time_rule = self.logic.time.has_lived_months(quantity // 14)
return self.logic.and_(opening_rule, mystery_box_rule,
book_of_mysteries_rule, time_rule,)
book_of_mysteries_rule, time_rule, )
def can_grind_artifact_troves(self, quantity: int) -> StardewRule:
opening_rule = self.logic.region.can_reach(Region.blacksmith)
@@ -67,11 +73,26 @@ class GrindLogic(BaseLogic[Union[GrindLogicMixin, HasLogicMixin, ReceivedLogicMi
# Assuming twelve per month if the player does not grind it.
self.logic.time.has_lived_months(quantity // 12))
def can_grind_item(self, quantity: int, item: str | None = None) -> StardewRule:
if item in EASY_ITEMS:
return self.logic.grind.can_grind_easy_item(quantity)
else:
return self.logic.grind.can_grind_medium_item(quantity)
@cache_self1
def can_grind_item(self, quantity: int) -> StardewRule:
if quantity <= MIN_ITEMS:
def can_grind_medium_item(self, quantity: int) -> StardewRule:
if quantity <= MIN_MEDIUM_ITEMS:
return self.logic.true_
quantity = min(quantity, MAX_ITEMS)
price = max(1, quantity * PERCENT_REQUIRED_FOR_MAX_ITEM // MAX_ITEMS)
quantity = min(quantity, MAX_MEDIUM_ITEMS)
price = max(1, quantity * PERCENT_REQUIRED_FOR_MAX_MEDIUM_ITEM // MAX_MEDIUM_ITEMS)
return HasProgressionPercent(self.player, price)
@cache_self1
def can_grind_easy_item(self, quantity: int) -> StardewRule:
if quantity <= MIN_EASY_ITEMS:
return self.logic.true_
quantity = min(quantity, MAX_EASY_ITEMS)
price = max(1, quantity * PERCENT_REQUIRED_FOR_MAX_EASY_ITEM // MAX_EASY_ITEMS)
return HasProgressionPercent(self.player, price)

View File

@@ -254,7 +254,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
Geode.omni: self.mine.can_mine_in_the_mines_floor_41_80() | self.region.can_reach(Region.desert) | self.tool.has_tool(Tool.pan, ToolMaterial.iron) | self.received(Wallet.rusty_key) | (self.has(Fish.octopus) & self.building.has_building(Building.fish_pond)) | self.region.can_reach(Region.volcano_floor_10),
Gift.bouquet: self.relationship.has_hearts_with_any_bachelor(8) & self.money.can_spend_at(Region.pierre_store, 100),
Gift.golden_pumpkin: self.season.has(Season.fall) | self.action.can_open_geode(Geode.artifact_trove),
Gift.mermaid_pendant: self.region.can_reach(Region.tide_pools) & self.relationship.has_hearts_with_any_bachelor(10) & self.building.has_house(1) & self.has(Consumable.rain_totem),
Gift.mermaid_pendant: self.region.can_reach(Region.tide_pools) & self.relationship.has_hearts_with_any_bachelor(10) & self.building.has_building(Building.kitchen) & self.has(Consumable.rain_totem),
Gift.movie_ticket: self.money.can_spend_at(Region.movie_ticket_stand, 1000),
Gift.pearl: (self.has(Fish.blobfish) & self.building.has_building(Building.fish_pond)) | self.action.can_open_geode(Geode.artifact_trove),
Gift.tea_set: self.season.has(Season.winter) & self.time.has_lived_max_months,
@@ -355,9 +355,6 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
obtention_rule = self.registry.item_rules[recipe] if recipe in self.registry.item_rules else False_()
self.registry.item_rules[recipe] = obtention_rule | crafting_rule
self.building.initialize_rules()
self.building.update_rules(self.mod.building.get_modded_building_rules())
self.quest.initialize_rules()
self.quest.update_rules(self.mod.quest.get_modded_quest_rules())

View File

@@ -17,8 +17,8 @@ from ..strings.region_names import Region, LogicRegion
if typing.TYPE_CHECKING:
from .shipping_logic import ShippingLogicMixin
assert ShippingLogicMixin
else:
ShippingLogicMixin = object
qi_gem_rewards = ("100 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems", "25 Qi Gems",
"20 Qi Gems", "15 Qi Gems", "10 Qi Gems")
@@ -31,7 +31,7 @@ class MoneyLogicMixin(BaseLogicMixin):
class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SeasonLogicMixin,
GrindLogicMixin, 'ShippingLogicMixin']]):
GrindLogicMixin, ShippingLogicMixin]]):
@cache_self1
def can_have_earned_total(self, amount: int) -> StardewRule:
@@ -80,7 +80,7 @@ GrindLogicMixin, 'ShippingLogicMixin']]):
item_rules = []
if source.items_price is not None:
for price, item in source.items_price:
item_rules.append(self.logic.has(item) & self.logic.grind.can_grind_item(price))
item_rules.append(self.logic.has(item) & self.logic.grind.can_grind_item(price, item))
region_rule = self.logic.region.can_reach(source.shop_region)

View File

@@ -15,9 +15,9 @@ from ..content.feature import friendsanity
from ..data.villagers_data import Villager
from ..stardew_rule import StardewRule, True_, false_, true_
from ..strings.ap_names.mods.mod_items import SVEQuestItem
from ..strings.building_names import Building
from ..strings.generic_names import Generic
from ..strings.gift_names import Gift
from ..strings.quest_names import ModQuest
from ..strings.region_names import Region
from ..strings.season_names import Season
from ..strings.villager_names import NPC, ModNPC
@@ -63,7 +63,7 @@ ReceivedLogicMixin, HasLogicMixin, ModLogicMixin]]):
if not self.content.features.friendsanity.is_enabled:
return self.logic.relationship.can_reproduce(number_children)
return self.logic.received_n(*possible_kids, count=number_children) & self.logic.building.has_house(2)
return self.logic.received_n(*possible_kids, count=number_children) & self.logic.building.has_building(Building.kids_room)
def can_reproduce(self, number_children: int = 1) -> StardewRule:
assert number_children >= 0, "Can't have a negative amount of children."
@@ -71,7 +71,7 @@ ReceivedLogicMixin, HasLogicMixin, ModLogicMixin]]):
return True_()
baby_rules = [self.logic.relationship.can_get_married(),
self.logic.building.has_house(2),
self.logic.building.has_building(Building.kids_room),
self.logic.relationship.has_hearts_with_any_bachelor(12),
self.logic.relationship.has_children(number_children - 1)]

View File

@@ -8,6 +8,7 @@ from .fishing_logic import FishingLogicMixin
from .has_logic import HasLogicMixin
from .quest_logic import QuestLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .relationship_logic import RelationshipLogicMixin
from .season_logic import SeasonLogicMixin
from .skill_logic import SkillLogicMixin
@@ -16,7 +17,7 @@ from .tool_logic import ToolLogicMixin
from .walnut_logic import WalnutLogicMixin
from ..data.game_item import Requirement
from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, CombatRequirement, QuestRequirement, \
RelationshipRequirement, FishingRequirement, WalnutRequirement
RelationshipRequirement, FishingRequirement, WalnutRequirement, RegionRequirement
class RequirementLogicMixin(BaseLogicMixin):
@@ -26,7 +27,7 @@ class RequirementLogicMixin(BaseLogicMixin):
class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin,
SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, RelationshipLogicMixin, FishingLogicMixin, WalnutLogicMixin]]):
SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, RelationshipLogicMixin, FishingLogicMixin, WalnutLogicMixin, RegionLogicMixin]]):
def meet_all_requirements(self, requirements: Iterable[Requirement]):
if not requirements:
@@ -45,6 +46,10 @@ SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, Relationshi
def _(self, requirement: SkillRequirement):
return self.logic.skill.has_level(requirement.skill, requirement.level)
@meet_requirement.register
def _(self, requirement: RegionRequirement):
return self.logic.region.can_reach(requirement.region)
@meet_requirement.register
def _(self, requirement: BookRequirement):
return self.logic.book.has_book_power(requirement.book)
@@ -76,5 +81,3 @@ SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, Relationshi
@meet_requirement.register
def _(self, requirement: FishingRequirement):
return self.logic.fishing.can_fish_at(requirement.region)

View File

@@ -12,7 +12,7 @@ from .region_logic import RegionLogicMixin
from .requirement_logic import RequirementLogicMixin
from .tool_logic import ToolLogicMixin
from ..data.artisan import MachineSource
from ..data.game_item import GenericSource, ItemSource, GameItem, CustomRuleSource, CompoundSource
from ..data.game_item import GenericSource, Source, GameItem, CustomRuleSource
from ..data.harvest import ForagingSource, FruitBatsSource, MushroomCaveSource, SeasonalForagingSource, \
HarvestCropSource, HarvestFruitTreeSource, ArtifactSpotSource
from ..data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
@@ -25,7 +25,7 @@ class SourceLogicMixin(BaseLogicMixin):
class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogicMixin, HarvestingLogicMixin, MoneyLogicMixin, RegionLogicMixin,
ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]):
ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]):
def has_access_to_item(self, item: GameItem):
rules = []
@@ -36,14 +36,10 @@ class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogic
rules.append(self.logic.source.has_access_to_any(item.sources))
return self.logic.and_(*rules)
def has_access_to_any(self, sources: Iterable[ItemSource]):
def has_access_to_any(self, sources: Iterable[Source]):
return self.logic.or_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements)
for source in sources))
def has_access_to_all(self, sources: Iterable[ItemSource]):
return self.logic.and_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements)
for source in sources))
@functools.singledispatchmethod
def has_access_to(self, source: Any):
raise ValueError(f"Sources of type{type(source)} have no rule registered.")
@@ -56,10 +52,6 @@ class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogic
def _(self, source: CustomRuleSource):
return source.create_rule(self.logic)
@has_access_to.register
def _(self, source: CompoundSource):
return self.logic.source.has_access_to_all(source.sources)
@has_access_to.register
def _(self, source: ForagingSource):
return self.logic.harvesting.can_forage_from(source)

View File

@@ -1,28 +0,0 @@
from typing import Dict, Union
from ..mod_data import ModNames
from ...logic.base_logic import BaseLogicMixin, BaseLogic
from ...logic.has_logic import HasLogicMixin
from ...logic.money_logic import MoneyLogicMixin
from ...stardew_rule import StardewRule
from ...strings.artisan_good_names import ArtisanGood
from ...strings.building_names import ModBuilding
from ...strings.metal_names import MetalBar
from ...strings.region_names import Region
class ModBuildingLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.building = ModBuildingLogic(*args, **kwargs)
class ModBuildingLogic(BaseLogic[Union[MoneyLogicMixin, HasLogicMixin]]):
def get_modded_building_rules(self) -> Dict[str, StardewRule]:
buildings = dict()
if ModNames.tractor in self.options.mods:
tractor_rule = (self.logic.money.can_spend_at(Region.carpenter, 150000) &
self.logic.has_all(MetalBar.iron, MetalBar.iridium, ArtisanGood.battery_pack))
buildings.update({ModBuilding.tractor_garage: tractor_rule})
return buildings

View File

@@ -1,4 +1,3 @@
from .buildings_logic import ModBuildingLogicMixin
from .deepwoods_logic import DeepWoodsLogicMixin
from .elevator_logic import ModElevatorLogicMixin
from .item_logic import ModItemLogicMixin
@@ -16,6 +15,6 @@ class ModLogicMixin(BaseLogicMixin):
self.mod = ModLogic(*args, **kwargs)
class ModLogic(ModElevatorLogicMixin, MagicLogicMixin, ModSkillLogicMixin, ModItemLogicMixin, ModQuestLogicMixin, ModBuildingLogicMixin,
class ModLogic(ModElevatorLogicMixin, MagicLogicMixin, ModSkillLogicMixin, ModItemLogicMixin, ModQuestLogicMixin,
ModSpecialOrderLogicMixin, DeepWoodsLogicMixin, SVELogicMixin):
pass

View File

@@ -19,7 +19,7 @@ from .logic.logic import StardewLogic
from .logic.time_logic import MAX_MONTHS
from .logic.tool_logic import tool_upgrade_prices
from .mods.mod_data import ModNames
from .options import BuildingProgression, ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \
from .options import ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \
Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity, StardewValleyOptions, Walnutsanity
from .stardew_rule import And, StardewRule, true_
from .stardew_rule.indirect_connection import look_for_indirect_connection
@@ -71,7 +71,7 @@ def set_rules(world):
set_tool_rules(logic, multiworld, player, world_content)
set_skills_rules(logic, multiworld, player, world_content)
set_bundle_rules(bundle_rooms, logic, multiworld, player, world_options)
set_building_rules(logic, multiworld, player, world_options)
set_building_rules(logic, multiworld, player, world_content)
set_cropsanity_rules(logic, multiworld, player, world_content)
set_story_quests_rules(all_location_names, logic, multiworld, player, world_options)
set_special_order_rules(all_location_names, logic, multiworld, player, world_options)
@@ -130,15 +130,19 @@ def set_tool_rules(logic: StardewLogic, multiworld, player, content: StardewCont
MultiWorldRules.set_rule(tool_upgrade_location, logic.tool.has_tool(tool, previous))
def set_building_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
if not world_options.building_progression & BuildingProgression.option_progressive:
def set_building_rules(logic: StardewLogic, multiworld, player, content: StardewContent):
building_progression = content.features.building_progression
if not building_progression.is_progressive:
return
for building in locations.locations_by_tag[LocationTags.BUILDING_BLUEPRINT]:
if building.mod_name is not None and building.mod_name not in world_options.mods:
for building in content.farm_buildings.values():
if building.name in building_progression.starting_buildings:
continue
MultiWorldRules.set_rule(multiworld.get_location(building.name, player),
logic.registry.building_rules[building.name.replace(" Blueprint", "")])
location_name = building_progression.to_location_name(building.name)
MultiWorldRules.set_rule(multiworld.get_location(location_name, player),
logic.building.can_build(building.name))
def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
@@ -241,7 +245,7 @@ def set_dangerous_mine_rules(logic, multiworld, player, world_options: StardewVa
def set_farm_buildings_entrance_rules(logic, multiworld, player):
set_entrance_rule(multiworld, player, Entrance.downstairs_to_cellar, logic.building.has_house(3))
set_entrance_rule(multiworld, player, Entrance.downstairs_to_cellar, logic.building.has_building(Building.cellar))
set_entrance_rule(multiworld, player, Entrance.use_desert_obelisk, logic.can_use_obelisk(Transportation.desert_obelisk))
set_entrance_rule(multiworld, player, Entrance.enter_greenhouse, logic.received("Greenhouse"))
set_entrance_rule(multiworld, player, Entrance.enter_coop, logic.building.has_building(Building.coop))

View File

@@ -14,9 +14,11 @@ class Building:
stable = "Stable"
well = "Well"
shipping_bin = "Shipping Bin"
farm_house = "Farm House"
kitchen = "Kitchen"
kids_room = "Kids Room"
cellar = "Cellar"
pet_bowl = "Pet Bowl"
class ModBuilding:

View File

@@ -61,11 +61,13 @@ class TestBooksanityNone(SVTestBase):
for location in self.multiworld.get_locations():
if not location.name.startswith(shipsanity_prefix):
continue
item_to_ship = location.name[len(shipsanity_prefix):]
if item_to_ship not in power_books and item_to_ship not in skill_books:
continue
with self.subTest(location.name):
self.assert_can_reach_location(location, self.multiworld.state)
self.assert_can_reach_location(location)
class TestBooksanityPowers(SVTestBase):
@@ -107,11 +109,13 @@ class TestBooksanityPowers(SVTestBase):
for location in self.multiworld.get_locations():
if not location.name.startswith(shipsanity_prefix):
continue
item_to_ship = location.name[len(shipsanity_prefix):]
if item_to_ship not in power_books and item_to_ship not in skill_books:
continue
with self.subTest(location.name):
self.assert_can_reach_location(location, self.multiworld.state)
self.assert_can_reach_location(location)
class TestBooksanityPowersAndSkills(SVTestBase):
@@ -153,11 +157,13 @@ class TestBooksanityPowersAndSkills(SVTestBase):
for location in self.multiworld.get_locations():
if not location.name.startswith(shipsanity_prefix):
continue
item_to_ship = location.name[len(shipsanity_prefix):]
if item_to_ship not in power_books and item_to_ship not in skill_books:
continue
with self.subTest(location.name):
self.assert_can_reach_location(location, self.multiworld.state)
self.assert_can_reach_location(location)
class TestBooksanityAll(SVTestBase):
@@ -199,8 +205,10 @@ class TestBooksanityAll(SVTestBase):
for location in self.multiworld.get_locations():
if not location.name.startswith(shipsanity_prefix):
continue
item_to_ship = location.name[len(shipsanity_prefix):]
if item_to_ship not in power_books and item_to_ship not in skill_books:
continue
with self.subTest(location.name):
self.assert_can_reach_location(location, self.multiworld.state)
self.assert_can_reach_location(location)

View File

@@ -1,5 +1,9 @@
from . import SVTestBase
from .. import options
from ..strings.ap_names.transport_names import Transportation
from ..strings.building_names import Building
from ..strings.region_names import Region
from ..strings.seed_names import Seed
class TestCropsanityRules(SVTestBase):
@@ -8,13 +12,13 @@ class TestCropsanityRules(SVTestBase):
}
def test_need_greenhouse_for_cactus(self):
harvest_cactus = self.world.logic.region.can_reach_location("Harvest Cactus Fruit")
self.assert_rule_false(harvest_cactus, self.multiworld.state)
harvest_cactus_fruit = "Harvest Cactus Fruit"
self.assert_cannot_reach_location(harvest_cactus_fruit)
self.multiworld.state.collect(self.create_item("Cactus Seeds"))
self.multiworld.state.collect(self.create_item("Shipping Bin"))
self.multiworld.state.collect(self.create_item("Desert Obelisk"))
self.assert_rule_false(harvest_cactus, self.multiworld.state)
self.multiworld.state.collect(self.create_item(Seed.cactus))
self.multiworld.state.collect(self.create_item(Building.shipping_bin))
self.multiworld.state.collect(self.create_item(Transportation.desert_obelisk))
self.assert_cannot_reach_location(harvest_cactus_fruit)
self.multiworld.state.collect(self.create_item("Greenhouse"))
self.assert_rule_true(harvest_cactus, self.multiworld.state)
self.multiworld.state.collect(self.create_item(Region.greenhouse))
self.assert_can_reach_location(harvest_cactus_fruit)

View File

@@ -1,3 +1,5 @@
from collections import Counter
from . import SVTestBase
from .assertion import WorldAssertMixin
from .. import options
@@ -5,27 +7,49 @@ from .. import options
class TestStartInventoryStandardFarm(WorldAssertMixin, SVTestBase):
options = {
options.FarmType.internal_name: options.FarmType.option_standard,
options.FarmType: options.FarmType.option_standard,
}
def test_start_inventory_progressive_coops(self):
start_items = dict(map(lambda x: (x.name, self.multiworld.precollected_items[self.player].count(x)), self.multiworld.precollected_items[self.player]))
items = dict(map(lambda x: (x.name, self.multiworld.itempool.count(x)), self.multiworld.itempool))
start_items = Counter((i.name for i in self.multiworld.precollected_items[self.player]))
items = Counter((i.name for i in self.multiworld.itempool))
self.assertIn("Progressive Coop", items)
self.assertEqual(items["Progressive Coop"], 3)
self.assertNotIn("Progressive Coop", start_items)
def test_coop_is_not_logically_available(self):
self.assert_rule_false(self.world.logic.building.has_building("Coop"))
class TestStartInventoryMeadowLands(WorldAssertMixin, SVTestBase):
class TestStartInventoryMeadowLandsProgressiveBuilding(WorldAssertMixin, SVTestBase):
options = {
options.FarmType.internal_name: options.FarmType.option_meadowlands,
options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive,
options.FarmType: options.FarmType.option_meadowlands,
options.BuildingProgression: options.BuildingProgression.option_progressive,
}
def test_start_inventory_progressive_coops(self):
start_items = dict(map(lambda x: (x.name, self.multiworld.precollected_items[self.player].count(x)), self.multiworld.precollected_items[self.player]))
items = dict(map(lambda x: (x.name, self.multiworld.itempool.count(x)), self.multiworld.itempool))
start_items = Counter((i.name for i in self.multiworld.precollected_items[self.player]))
items = Counter((i.name for i in self.multiworld.itempool))
self.assertIn("Progressive Coop", items)
self.assertEqual(items["Progressive Coop"], 2)
self.assertIn("Progressive Coop", start_items)
self.assertEqual(start_items["Progressive Coop"], 1)
def test_coop_is_logically_available(self):
self.assert_rule_true(self.world.logic.building.has_building("Coop"))
class TestStartInventoryMeadowLandsVanillaBuildings(WorldAssertMixin, SVTestBase):
options = {
options.FarmType: options.FarmType.option_meadowlands,
options.BuildingProgression: options.BuildingProgression.option_vanilla,
}
def test_start_inventory_has_no_coop(self):
start_items = Counter((i.name for i in self.multiworld.precollected_items[self.player]))
self.assertNotIn("Progressive Coop", start_items)
def test_coop_is_logically_available(self):
self.assert_rule_true(self.world.logic.building.has_building("Coop"))

View File

@@ -3,13 +3,30 @@ from typing import List
from BaseClasses import ItemClassification, Item
from . import SVTestBase
from .. import items, location_table, options
from ..items import Group
from ..items import Group, ItemData
from ..locations import LocationTags
from ..options import Friendsanity, SpecialOrderLocations, Shipsanity, Chefsanity, SeasonRandomization, Craftsanity, ExcludeGingerIsland, SkillProgression, \
Booksanity, Walnutsanity
from ..strings.region_names import Region
def get_all_permanent_progression_items() -> List[ItemData]:
"""Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression.
"""
return [
item
for item in items.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 {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]}
if item.name not in {baby.name for baby in items.items_by_group[Group.BABY]}
if item.name != "The Gateway Gazette"
]
class TestBaseItemGeneration(SVTestBase):
options = {
SeasonRandomization.internal_name: SeasonRandomization.option_progressive,
@@ -25,17 +42,8 @@ class TestBaseItemGeneration(SVTestBase):
}
def test_all_progression_items_are_added_to_the_pool(self):
all_created_items = [item.name for item in self.multiworld.itempool]
# Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression
items_to_ignore = [event.name for event in items.events]
items_to_ignore.extend(item.name for item in items.all_items if item.mod_name is not None)
items_to_ignore.extend(deprecated.name for deprecated in items.items_by_group[Group.DEPRECATED])
items_to_ignore.extend(season.name for season in items.items_by_group[Group.SEASON])
items_to_ignore.extend(weapon.name for weapon in items.items_by_group[Group.WEAPON])
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK])
items_to_ignore.append("The Gateway Gazette")
progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore]
all_created_items = set(self.get_all_created_items())
progression_items = get_all_permanent_progression_items()
for progression_item in progression_items:
with self.subTest(f"{progression_item.name}"):
self.assertIn(progression_item.name, all_created_items)
@@ -45,19 +53,19 @@ class TestBaseItemGeneration(SVTestBase):
self.assertEqual(len(non_event_locations), len(self.multiworld.itempool))
def test_does_not_create_deprecated_items(self):
all_created_items = [item.name for item in self.multiworld.itempool]
all_created_items = set(self.get_all_created_items())
for deprecated_item in items.items_by_group[items.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 = [item.name for item in self.multiworld.itempool]
all_created_items = self.get_all_created_items()
for maximum_one_item in items.items_by_group[items.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 = [item.name for item in self.multiworld.itempool]
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.EXACTLY_TWO]:
with self.subTest(f"{exactly_two_item.name}"):
count = all_created_items.count(exactly_two_item.name)
@@ -77,17 +85,10 @@ class TestNoGingerIslandItemGeneration(SVTestBase):
}
def test_all_progression_items_except_island_are_added_to_the_pool(self):
all_created_items = [item.name for item in self.multiworld.itempool]
# Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression
items_to_ignore = [event.name for event in items.events]
items_to_ignore.extend(item.name for item in items.all_items if item.mod_name is not None)
items_to_ignore.extend(deprecated.name for deprecated in items.items_by_group[Group.DEPRECATED])
items_to_ignore.extend(season.name for season in items.items_by_group[Group.SEASON])
items_to_ignore.extend(season.name for season in items.items_by_group[Group.WEAPON])
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
items_to_ignore.append("The Gateway Gazette")
progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore]
all_created_items = set(self.get_all_created_items())
progression_items = get_all_permanent_progression_items()
for progression_item in progression_items:
with self.subTest(f"{progression_item.name}"):
if Group.GINGER_ISLAND in progression_item.groups:
self.assertNotIn(progression_item.name, all_created_items)
@@ -100,19 +101,19 @@ class TestNoGingerIslandItemGeneration(SVTestBase):
self.assertEqual(len(non_event_locations), len(self.multiworld.itempool))
def test_does_not_create_deprecated_items(self):
all_created_items = [item.name for item in self.multiworld.itempool]
all_created_items = self.get_all_created_items()
for deprecated_item in items.items_by_group[items.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 = [item.name for item in self.multiworld.itempool]
all_created_items = self.get_all_created_items()
for maximum_one_item in items.items_by_group[items.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 = [item.name for item in self.multiworld.itempool]
all_created_items = self.get_all_created_items()
for exactly_two_item in items.items_by_group[items.Group.EXACTLY_TWO]:
with self.subTest(f"{exactly_two_item.name}"):
count = all_created_items.count(exactly_two_item.name)

View File

@@ -49,9 +49,9 @@ class LogicTestBase(RuleAssertMixin, TestCase):
self.assert_rule_can_be_resolved(rule, self.multiworld.state)
def test_given_building_rule_then_can_be_resolved(self):
for building in self.logic.registry.building_rules.keys():
for building in self.world.content.farm_buildings:
with self.subTest(msg=building):
rule = self.logic.registry.building_rules[building]
rule = self.logic.building.can_build(building)
self.assert_rule_can_be_resolved(rule, self.multiworld.state)
def test_given_quest_rule_then_can_be_resolved(self):

View File

@@ -8,9 +8,8 @@ class TestBitFlagsVanilla(SVTestBase):
BuildingProgression.internal_name: BuildingProgression.option_vanilla}
def test_options_are_not_detected_as_progressive(self):
world_options = self.world.options
tool_progressive = self.world.content.features.tool_progression.is_progressive
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
building_progressive = self.world.content.features.building_progression.is_progressive
self.assertFalse(tool_progressive)
self.assertFalse(building_progressive)
@@ -25,9 +24,8 @@ class TestBitFlagsVanillaCheap(SVTestBase):
BuildingProgression.internal_name: BuildingProgression.option_vanilla_cheap}
def test_options_are_not_detected_as_progressive(self):
world_options = self.world.options
tool_progressive = self.world.content.features.tool_progression.is_progressive
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
building_progressive = self.world.content.features.building_progression.is_progressive
self.assertFalse(tool_progressive)
self.assertFalse(building_progressive)
@@ -42,9 +40,8 @@ class TestBitFlagsVanillaVeryCheap(SVTestBase):
BuildingProgression.internal_name: BuildingProgression.option_vanilla_very_cheap}
def test_options_are_not_detected_as_progressive(self):
world_options = self.world.options
tool_progressive = self.world.content.features.tool_progression.is_progressive
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
building_progressive = self.world.content.features.building_progression.is_progressive
self.assertFalse(tool_progressive)
self.assertFalse(building_progressive)
@@ -59,9 +56,8 @@ class TestBitFlagsProgressive(SVTestBase):
BuildingProgression.internal_name: BuildingProgression.option_progressive}
def test_options_are_detected_as_progressive(self):
world_options = self.world.options
tool_progressive = self.world.content.features.tool_progression.is_progressive
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
building_progressive = self.world.content.features.building_progression.is_progressive
self.assertTrue(tool_progressive)
self.assertTrue(building_progressive)
@@ -76,9 +72,8 @@ class TestBitFlagsProgressiveCheap(SVTestBase):
BuildingProgression.internal_name: BuildingProgression.option_progressive_cheap}
def test_options_are_detected_as_progressive(self):
world_options = self.world.options
tool_progressive = self.world.content.features.tool_progression.is_progressive
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
building_progressive = self.world.content.features.building_progression.is_progressive
self.assertTrue(tool_progressive)
self.assertTrue(building_progressive)
@@ -93,9 +88,8 @@ class TestBitFlagsProgressiveVeryCheap(SVTestBase):
BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap}
def test_options_are_detected_as_progressive(self):
world_options = self.world.options
tool_progressive = self.world.content.features.tool_progression.is_progressive
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
building_progressive = self.world.content.features.building_progression.is_progressive
self.assertTrue(tool_progressive)
self.assertTrue(building_progressive)

View File

@@ -70,7 +70,6 @@ class TestWalnutsanityPuzzles(SVTestBase):
def test_field_office_locations_require_professor_snail(self):
location_names = ["Complete Large Animal Collection", "Complete Snake Collection", "Complete Mummified Frog Collection",
"Complete Mummified Bat Collection", "Purple Flowers Island Survey", "Purple Starfish Island Survey", ]
locations = [location for location in self.multiworld.get_locations() if location.name in location_names]
self.collect("Island Obelisk")
self.collect("Island North Turtle")
self.collect("Island West Turtle")
@@ -84,11 +83,11 @@ class TestWalnutsanityPuzzles(SVTestBase):
self.collect("Progressive Sword", 5)
self.collect("Combat Level", 10)
self.collect("Mining Level", 10)
for location in locations:
self.assert_cannot_reach_location(location, self.multiworld.state)
for location in location_names:
self.assert_cannot_reach_location(location)
self.collect("Open Professor Snail Cave")
for location in locations:
self.assert_can_reach_location(location, self.multiworld.state)
for location in location_names:
self.assert_can_reach_location(location)
class TestWalnutsanityBushes(SVTestBase):

View File

@@ -1,3 +1,4 @@
import itertools
import logging
import os
import threading
@@ -11,7 +12,8 @@ from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_mul
from worlds.AutoWorld import call_all
from .assertion import RuleAssertMixin
from .options.utils import fill_namespace_with_default, parse_class_option_keys, fill_dataclass_with_default
from .. import StardewValleyWorld, StardewItem
from .. import StardewValleyWorld, StardewItem, StardewRule
from ..logic.time_logic import MONTH_COEFFICIENT
from ..options import StardewValleyOption
logger = logging.getLogger(__name__)
@@ -96,6 +98,12 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
return False
return super().run_default_tests
def collect_months(self, months: int) -> None:
real_total_prog_items = self.world.total_progression_items
percent = months * MONTH_COEFFICIENT
self.collect("Stardrop", real_total_prog_items * 100 // percent)
self.world.total_progression_items = real_total_prog_items
def collect_lots_of_money(self, percent: float = 0.25):
self.collect("Shipping Bin")
real_total_prog_items = self.world.total_progression_items
@@ -145,12 +153,35 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
def create_item(self, item: str) -> StardewItem:
return self.world.create_item(item)
def get_all_created_items(self) -> list[str]:
return [item.name for item in itertools.chain(self.multiworld.get_items(), self.multiworld.precollected_items[self.player])]
def remove_one_by_name(self, item: str) -> None:
self.remove(self.create_item(item))
def reset_collection_state(self):
def reset_collection_state(self) -> None:
self.multiworld.state = self.original_state.copy()
def assert_rule_true(self, rule: StardewRule, state: CollectionState | None = None) -> None:
if state is None:
state = self.multiworld.state
super().assert_rule_true(rule, state)
def assert_rule_false(self, rule: StardewRule, state: CollectionState | None = None) -> None:
if state is None:
state = self.multiworld.state
super().assert_rule_false(rule, state)
def assert_can_reach_location(self, location: Location | str, state: CollectionState | None = None) -> None:
if state is None:
state = self.multiworld.state
super().assert_can_reach_location(location, state)
def assert_cannot_reach_location(self, location: Location | str, state: CollectionState | None = None) -> None:
if state is None:
state = self.multiworld.state
super().assert_cannot_reach_location(location, state)
pre_generated_worlds = {}

View File

@@ -2,9 +2,11 @@ import unittest
from typing import ClassVar, Tuple
from ...content import content_packs, ContentPack, StardewContent, unpack_content, StardewFeatures, feature
from ...strings.building_names import Building
default_features = StardewFeatures(
feature.booksanity.BooksanityDisabled(),
feature.building_progression.BuildingProgressionVanilla(starting_buildings={Building.farm_house}),
feature.cropsanity.CropsanityDisabled(),
feature.fishsanity.FishsanityNone(),
feature.friendsanity.FriendsanityNone(),

View File

@@ -6,7 +6,6 @@ from .option_names import all_option_choices
from .. import SVTestCase, solo_multiworld
from ..assertion.world_assert import WorldAssertMixin
from ... import options
from ...mods.mod_data import ModNames
class TestGenerateDynamicOptions(WorldAssertMixin, SVTestCase):
@@ -34,13 +33,11 @@ class TestDynamicOptionDebug(WorldAssertMixin, SVTestCase):
def test_option_pair_debug(self):
option_dict = {
options.Goal.internal_name: options.Goal.option_master_angler,
options.QuestLocations.internal_name: -1,
options.Fishsanity.internal_name: options.Fishsanity.option_all,
options.Mods.internal_name: frozenset({ModNames.sve}),
options.Goal.internal_name: options.Goal.option_cryptic_note,
options.EntranceRandomization.internal_name: options.EntranceRandomization.option_buildings,
}
for i in range(1):
seed = get_seed()
seed = get_seed(76312028554502615508)
with self.subTest(f"Seed: {seed}"):
print(f"Seed: {seed}")
with solo_multiworld(option_dict, seed=seed) as (multiworld, _):

View File

@@ -1,11 +1,12 @@
import random
from BaseClasses import get_seed, ItemClassification
from BaseClasses import get_seed
from .. import SVTestBase, SVTestCase
from ..TestGeneration import get_all_permanent_progression_items
from ..assertion import ModAssertMixin, WorldAssertMixin
from ..options.presets import allsanity_mods_6_x_x
from ..options.utils import fill_dataclass_with_default
from ... import options, items, Group, create_content
from ... import options, Group, create_content
from ...mods.mod_data import ModNames
from ...options import SkillProgression, Walnutsanity
from ...options.options import all_mods
@@ -109,17 +110,8 @@ class TestBaseItemGeneration(SVTestBase):
}
def test_all_progression_items_are_added_to_the_pool(self):
all_created_items = [item.name for item in self.multiworld.itempool]
# Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression
items_to_ignore = [event.name for event in items.events]
items_to_ignore.extend(deprecated.name for deprecated in items.items_by_group[Group.DEPRECATED])
items_to_ignore.extend(season.name for season in items.items_by_group[Group.SEASON])
items_to_ignore.extend(weapon.name for weapon in items.items_by_group[Group.WEAPON])
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK])
items_to_ignore.append("The Gateway Gazette")
progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression
and item.name not in items_to_ignore]
all_created_items = self.get_all_created_items()
progression_items = get_all_permanent_progression_items()
for progression_item in progression_items:
with self.subTest(f"{progression_item.name}"):
self.assertIn(progression_item.name, all_created_items)
@@ -139,17 +131,8 @@ class TestNoGingerIslandModItemGeneration(SVTestBase):
}
def test_all_progression_items_except_island_are_added_to_the_pool(self):
all_created_items = [item.name for item in self.multiworld.itempool]
# Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression
items_to_ignore = [event.name for event in items.events]
items_to_ignore.extend(deprecated.name for deprecated in items.items_by_group[Group.DEPRECATED])
items_to_ignore.extend(season.name for season in items.items_by_group[Group.SEASON])
items_to_ignore.extend(weapon.name for weapon in items.items_by_group[Group.WEAPON])
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK])
items_to_ignore.append("The Gateway Gazette")
progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression
and item.name not in items_to_ignore]
all_created_items = self.get_all_created_items()
progression_items = get_all_permanent_progression_items()
for progression_item in progression_items:
with self.subTest(f"{progression_item.name}"):
if Group.GINGER_ISLAND in progression_item.groups:

View File

@@ -16,6 +16,7 @@ def default_6_x_x():
options.ElevatorProgression.internal_name: options.ElevatorProgression.default,
options.EntranceRandomization.internal_name: options.EntranceRandomization.default,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.default,
options.FarmType.internal_name: options.FarmType.default,
options.FestivalLocations.internal_name: options.FestivalLocations.default,
options.Fishsanity.internal_name: options.Fishsanity.default,
options.Friendsanity.internal_name: options.Friendsanity.default,
@@ -52,6 +53,7 @@ def allsanity_no_mods_6_x_x():
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive,
options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
options.FarmType.internal_name: options.FarmType.option_standard,
options.FestivalLocations.internal_name: options.FestivalLocations.option_hard,
options.Fishsanity.internal_name: options.Fishsanity.option_all,
options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
@@ -100,6 +102,7 @@ def get_minsanity_options():
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla,
options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
options.FarmType.internal_name: options.FarmType.option_meadowlands,
options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled,
options.Fishsanity.internal_name: options.Fishsanity.option_none,
options.Friendsanity.internal_name: options.Friendsanity.option_none,
@@ -136,6 +139,7 @@ def minimal_locations_maximal_items():
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla,
options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
options.FarmType.internal_name: options.FarmType.option_meadowlands,
options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled,
options.Fishsanity.internal_name: options.Fishsanity.option_none,
options.Friendsanity.internal_name: options.Friendsanity.option_none,

View File

@@ -11,7 +11,7 @@ class TestArcadeMachinesLogic(SVTestBase):
self.assertFalse(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state))
self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state))
self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state))
self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
self.assert_cannot_reach_location("Journey of the Prairie King Victory")
boots = self.create_item("JotPK: Progressive Boots")
gun = self.create_item("JotPK: Progressive Gun")
@@ -24,7 +24,7 @@ class TestArcadeMachinesLogic(SVTestBase):
self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state))
self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state))
self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state))
self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
self.assert_cannot_reach_location("Journey of the Prairie King Victory")
self.remove(boots)
self.remove(gun)
@@ -33,7 +33,7 @@ class TestArcadeMachinesLogic(SVTestBase):
self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state))
self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state))
self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state))
self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
self.assert_cannot_reach_location("Journey of the Prairie King Victory")
self.remove(boots)
self.remove(boots)
@@ -44,7 +44,7 @@ class TestArcadeMachinesLogic(SVTestBase):
self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state))
self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state))
self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state))
self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
self.assert_cannot_reach_location("Journey of the Prairie King Victory")
self.remove(boots)
self.remove(gun)
self.remove(ammo)
@@ -60,7 +60,7 @@ class TestArcadeMachinesLogic(SVTestBase):
self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state))
self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state))
self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state))
self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
self.assert_cannot_reach_location("Journey of the Prairie King Victory")
self.remove(boots)
self.remove(gun)
self.remove(gun)
@@ -83,7 +83,7 @@ class TestArcadeMachinesLogic(SVTestBase):
self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state))
self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state))
self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state))
self.assertTrue(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
self.assert_can_reach_location("Journey of the Prairie King Victory")
self.remove(boots)
self.remove(boots)
self.remove(gun)

View File

@@ -7,18 +7,15 @@ class TestBooksLogic(SVTestBase):
options.Booksanity.internal_name: options.Booksanity.option_all,
}
def test_need_weapon_for_mapping_cave_systems(self):
self.collect_lots_of_money(0.5)
location = self.multiworld.get_location("Read Mapping Cave Systems", self.player)
self.assert_cannot_reach_location(location, self.multiworld.state)
def test_can_get_mapping_cave_systems_with_weapon_and_time(self):
self.collect_months(12)
self.assert_cannot_reach_location("Read Mapping Cave Systems")
self.collect("Progressive Mine Elevator")
self.collect("Progressive Mine Elevator")
self.collect("Progressive Mine Elevator")
self.collect("Progressive Mine Elevator")
self.assert_cannot_reach_location(location, self.multiworld.state)
self.assert_cannot_reach_location("Read Mapping Cave Systems")
self.collect("Progressive Weapon")
self.assert_can_reach_location(location, self.multiworld.state)
self.assert_can_reach_location("Read Mapping Cave Systems")

View File

@@ -9,45 +9,37 @@ class TestBuildingLogic(SVTestBase):
}
def test_coop_blueprint(self):
self.assertFalse(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state))
self.assert_cannot_reach_location("Coop Blueprint")
self.collect_lots_of_money()
self.assertTrue(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state))
self.assert_can_reach_location("Coop Blueprint")
def test_big_coop_blueprint(self):
big_coop_blueprint_rule = self.world.logic.region.can_reach_location("Big Coop Blueprint")
self.assertFalse(big_coop_blueprint_rule(self.multiworld.state),
f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}")
self.assert_cannot_reach_location("Big Coop Blueprint")
self.collect_lots_of_money()
self.assertFalse(big_coop_blueprint_rule(self.multiworld.state),
f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}")
self.assert_cannot_reach_location("Big Coop Blueprint")
self.multiworld.state.collect(self.create_item("Progressive Coop"))
self.assertTrue(big_coop_blueprint_rule(self.multiworld.state),
f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}")
self.assert_can_reach_location("Big Coop Blueprint")
def test_deluxe_coop_blueprint(self):
self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state))
self.assert_cannot_reach_location("Deluxe Coop Blueprint")
self.collect_lots_of_money()
self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state))
self.assert_cannot_reach_location("Deluxe Coop Blueprint")
self.multiworld.state.collect(self.create_item("Progressive Coop"))
self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state))
self.assert_cannot_reach_location("Deluxe Coop Blueprint")
self.multiworld.state.collect(self.create_item("Progressive Coop"))
self.assertTrue(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state))
self.assert_can_reach_location("Deluxe Coop Blueprint")
def test_big_shed_blueprint(self):
big_shed_rule = self.world.logic.region.can_reach_location("Big Shed Blueprint")
self.assertFalse(big_shed_rule(self.multiworld.state),
f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}")
self.assert_cannot_reach_location("Big Shed Blueprint")
self.collect_lots_of_money()
self.assertFalse(big_shed_rule(self.multiworld.state),
f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}")
self.assert_cannot_reach_location("Big Shed Blueprint")
self.multiworld.state.collect(self.create_item("Progressive Shed"))
self.assertTrue(big_shed_rule(self.multiworld.state),
f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}")
self.assert_can_reach_location("Big Shed Blueprint")

View File

@@ -11,10 +11,10 @@ class TestBundlesLogic(SVTestBase):
}
def test_vault_2500g_bundle(self):
self.assertFalse(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state))
self.assert_cannot_reach_location("2,500g Bundle")
self.collect_lots_of_money()
self.assertTrue(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state))
self.assert_can_reach_location("2,500g Bundle")
class TestRemixedBundlesLogic(SVTestBase):
@@ -25,10 +25,10 @@ class TestRemixedBundlesLogic(SVTestBase):
}
def test_sticky_bundle_has_grind_rules(self):
self.assertFalse(self.world.logic.region.can_reach_location("Sticky Bundle")(self.multiworld.state))
self.assert_cannot_reach_location("Sticky Bundle")
self.collect_all_the_money()
self.assertTrue(self.world.logic.region.can_reach_location("Sticky Bundle")(self.multiworld.state))
self.assert_can_reach_location("Sticky Bundle")
class TestRaccoonBundlesLogic(SVTestBase):
@@ -40,11 +40,6 @@ class TestRaccoonBundlesLogic(SVTestBase):
seed = 2 # Magic seed that does what I want. Might need to get changed if we change the randomness behavior of raccoon bundles
def test_raccoon_bundles_rely_on_previous_ones(self):
# The first raccoon bundle is a fishing one
raccoon_rule_1 = self.world.logic.region.can_reach_location("Raccoon Request 1")
# The 3th raccoon bundle is a foraging one
raccoon_rule_3 = self.world.logic.region.can_reach_location("Raccoon Request 3")
self.collect("Progressive Raccoon", 6)
self.collect("Progressive Mine Elevator", 24)
self.collect("Mining Level", 12)
@@ -58,10 +53,12 @@ class TestRaccoonBundlesLogic(SVTestBase):
self.collect("Fishing Level", 10)
self.collect("Furnace Recipe")
self.assertFalse(raccoon_rule_1(self.multiworld.state))
self.assertFalse(raccoon_rule_3(self.multiworld.state))
# The first raccoon bundle is a fishing one
self.assert_cannot_reach_location("Raccoon Request 1")
# The third raccoon bundle is a foraging one
self.assert_cannot_reach_location("Raccoon Request 3")
self.collect("Fish Smoker Recipe")
self.assertTrue(raccoon_rule_1(self.multiworld.state))
self.assertTrue(raccoon_rule_3(self.multiworld.state))
self.assert_can_reach_location("Raccoon Request 1")
self.assert_can_reach_location("Raccoon Request 3")

View File

@@ -14,18 +14,17 @@ class TestRecipeLearnLogic(SVTestBase):
def test_can_learn_qos_recipe(self):
location = "Cook Radish Salad"
rule = self.world.logic.region.can_reach_location(location)
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location(location)
self.multiworld.state.collect(self.create_item("Progressive House"))
self.multiworld.state.collect(self.create_item("Radish Seeds"))
self.multiworld.state.collect(self.create_item("Spring"))
self.multiworld.state.collect(self.create_item("Summer"))
self.collect_lots_of_money()
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location(location)
self.multiworld.state.collect(self.create_item("The Queen of Sauce"))
self.assert_rule_true(rule, self.multiworld.state)
self.assert_can_reach_location(location)
class TestRecipeReceiveLogic(SVTestBase):
@@ -39,34 +38,32 @@ class TestRecipeReceiveLogic(SVTestBase):
def test_can_learn_qos_recipe(self):
location = "Cook Radish Salad"
rule = self.world.logic.region.can_reach_location(location)
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location(location)
self.multiworld.state.collect(self.create_item("Progressive House"))
self.multiworld.state.collect(self.create_item("Radish Seeds"))
self.multiworld.state.collect(self.create_item("Summer"))
self.collect_lots_of_money()
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location(location)
spring = self.create_item("Spring")
qos = self.create_item("The Queen of Sauce")
self.multiworld.state.collect(spring)
self.multiworld.state.collect(qos)
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location(location)
self.multiworld.state.remove(spring)
self.multiworld.state.remove(qos)
self.multiworld.state.collect(self.create_item("Radish Salad Recipe"))
self.assert_rule_true(rule, self.multiworld.state)
self.assert_can_reach_location(location)
def test_get_chefsanity_check_recipe(self):
location = "Radish Salad Recipe"
rule = self.world.logic.region.can_reach_location(location)
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location(location)
self.multiworld.state.collect(self.create_item("Spring"))
self.collect_lots_of_money()
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location(location)
seeds = self.create_item("Radish Seeds")
summer = self.create_item("Summer")
@@ -74,10 +71,10 @@ class TestRecipeReceiveLogic(SVTestBase):
self.multiworld.state.collect(seeds)
self.multiworld.state.collect(summer)
self.multiworld.state.collect(house)
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location(location)
self.multiworld.state.remove(seeds)
self.multiworld.state.remove(summer)
self.multiworld.state.remove(house)
self.multiworld.state.collect(self.create_item("The Queen of Sauce"))
self.assert_rule_true(rule, self.multiworld.state)
self.assert_can_reach_location(location)

View File

@@ -13,8 +13,6 @@ class TestCraftsanityLogic(SVTestBase):
}
def test_can_craft_recipe(self):
location = "Craft Marble Brazier"
rule = self.world.logic.region.can_reach_location(location)
self.collect([self.create_item("Progressive Pickaxe")] * 4)
self.collect([self.create_item("Progressive Fishing Rod")] * 4)
self.collect([self.create_item("Progressive Sword")] * 4)
@@ -23,18 +21,16 @@ class TestCraftsanityLogic(SVTestBase):
self.collect([self.create_item("Combat Level")] * 10)
self.collect([self.create_item("Fishing Level")] * 10)
self.collect_all_the_money()
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location("Craft Marble Brazier")
self.multiworld.state.collect(self.create_item("Marble Brazier Recipe"))
self.assert_rule_true(rule, self.multiworld.state)
self.assert_can_reach_location("Craft Marble Brazier")
def test_can_learn_crafting_recipe(self):
location = "Marble Brazier Recipe"
rule = self.world.logic.region.can_reach_location(location)
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location("Marble Brazier Recipe")
self.collect_lots_of_money()
self.assert_rule_true(rule, self.multiworld.state)
self.assert_can_reach_location("Marble Brazier Recipe")
def test_can_craft_festival_recipe(self):
recipe = all_crafting_recipes_by_name["Jack-O-Lantern"]
@@ -42,13 +38,13 @@ class TestCraftsanityLogic(SVTestBase):
self.multiworld.state.collect(self.create_item("Torch Recipe"))
self.collect_lots_of_money()
rule = self.world.logic.crafting.can_craft(recipe)
self.assert_rule_false(rule, self.multiworld.state)
self.assert_rule_false(rule)
self.multiworld.state.collect(self.create_item("Fall"))
self.assert_rule_false(rule, self.multiworld.state)
self.assert_rule_false(rule)
self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"))
self.assert_rule_true(rule, self.multiworld.state)
self.assert_rule_true(rule)
def test_require_furnace_recipe_for_smelting_checks(self):
locations = ["Craft Furnace", "Smelting", "Copper Pickaxe Upgrade", "Gold Trash Can Upgrade"]
@@ -83,13 +79,13 @@ class TestCraftsanityWithFestivalsLogic(SVTestBase):
self.multiworld.state.collect(self.create_item("Fall"))
self.collect_lots_of_money()
rule = self.world.logic.crafting.can_craft(recipe)
self.assert_rule_false(rule, self.multiworld.state)
self.assert_rule_false(rule)
self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"))
self.assert_rule_false(rule, self.multiworld.state)
self.assert_rule_false(rule)
self.multiworld.state.collect(self.create_item("Torch Recipe"))
self.assert_rule_true(rule, self.multiworld.state)
self.assert_rule_true(rule)
class TestNoCraftsanityLogic(SVTestBase):
@@ -105,7 +101,7 @@ class TestNoCraftsanityLogic(SVTestBase):
def test_can_craft_recipe(self):
recipe = all_crafting_recipes_by_name["Wood Floor"]
rule = self.world.logic.crafting.can_craft(recipe)
self.assert_rule_true(rule, self.multiworld.state)
self.assert_rule_true(rule)
def test_can_craft_festival_recipe(self):
recipe = all_crafting_recipes_by_name["Jack-O-Lantern"]
@@ -116,7 +112,7 @@ class TestNoCraftsanityLogic(SVTestBase):
self.assertFalse(result)
self.collect([self.create_item("Progressive Season")] * 2)
self.assert_rule_true(rule, self.multiworld.state)
self.assert_rule_true(rule)
def test_requires_mining_levels_for_smelting_checks(self):
locations = ["Smelting", "Copper Pickaxe Upgrade", "Gold Trash Can Upgrade"]
@@ -151,7 +147,7 @@ class TestNoCraftsanityWithFestivalsLogic(SVTestBase):
self.multiworld.state.collect(self.create_item("Fall"))
self.collect_lots_of_money()
rule = self.world.logic.crafting.can_craft(recipe)
self.assert_rule_false(rule, self.multiworld.state)
self.assert_rule_false(rule)
self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"))
self.assert_rule_true(rule, self.multiworld.state)
self.assert_rule_true(rule)

View File

@@ -16,12 +16,12 @@ class TestDonationLogicAll(SVTestBase):
self.collect_all_except(railroad_item)
for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]:
self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state))
self.assert_cannot_reach_location(donation.name)
self.multiworld.state.collect(self.create_item(railroad_item))
for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]:
self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state))
self.assert_can_reach_location(donation.name)
class TestDonationLogicRandomized(SVTestBase):
@@ -37,12 +37,12 @@ class TestDonationLogicRandomized(SVTestBase):
LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags]
for donation in donation_locations:
self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state))
self.assert_cannot_reach_location(donation.name)
self.multiworld.state.collect(self.create_item(railroad_item))
for donation in donation_locations:
self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state))
self.assert_can_reach_location(donation.name)
class TestDonationLogicMilestones(SVTestBase):
@@ -56,12 +56,12 @@ class TestDonationLogicMilestones(SVTestBase):
self.collect_all_except(railroad_item)
for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]:
self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state))
self.assert_cannot_reach_location(donation.name)
self.multiworld.state.collect(self.create_item(railroad_item))
for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]:
self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state))
self.assert_can_reach_location(donation.name)
def swap_museum_and_bathhouse(multiworld, player):

View File

@@ -42,19 +42,18 @@ class TestNeedRegionToCatchFish(SVTestBase):
with self.subTest(f"Region rules for {fish}"):
self.collect_all_the_money()
item_names = fish_and_items[fish]
location = self.multiworld.get_location(f"Fishsanity: {fish}", self.player)
self.assert_cannot_reach_location(location, self.multiworld.state)
self.assert_cannot_reach_location(f"Fishsanity: {fish}")
items = []
for item_name in item_names:
items.append(self.collect(item_name))
with self.subTest(f"{fish} can be reached with {item_names}"):
self.assert_can_reach_location(location, self.multiworld.state)
self.assert_can_reach_location(f"Fishsanity: {fish}")
for item_required in items:
self.multiworld.state = self.original_state.copy()
with self.subTest(f"{fish} requires {item_required.name}"):
for item_to_collect in items:
if item_to_collect.name != item_required.name:
self.collect(item_to_collect)
self.assert_cannot_reach_location(location, self.multiworld.state)
self.assert_cannot_reach_location(f"Fishsanity: {fish}")
self.multiworld.state = self.original_state.copy()

View File

@@ -47,12 +47,8 @@ class TestFriendsanityDatingRules(SVTestBase):
for i in range(1, max_reachable + 1):
if i % step != 0 and i != 14:
continue
location = f"{prefix}{npc} {i}{suffix}"
can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state)
self.assertTrue(can_reach, f"Should be able to earn relationship up to {i} hearts")
self.assert_can_reach_location(f"{prefix}{npc} {i}{suffix}")
for i in range(max_reachable + 1, 14 + 1):
if i % step != 0 and i != 14:
continue
location = f"{prefix}{npc} {i}{suffix}"
can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state)
self.assertFalse(can_reach, f"Should not be able to earn relationship up to {i} hearts")
self.assert_cannot_reach_location(f"{prefix}{npc} {i}{suffix}")

View File

@@ -76,10 +76,8 @@ class TestShipsanityEverything(SVTestBase):
for location in shipsanity_locations:
with self.subTest(location.name):
self.assertFalse(self.world.logic.region.can_reach_location(location.name)(self.multiworld.state))
self.assert_cannot_reach_location(location.name)
self.collect(bin_item)
shipsanity_rule = self.world.logic.region.can_reach_location(location.name)
self.assert_rule_true(shipsanity_rule, self.multiworld.state)
self.assert_can_reach_location(location.name)
self.remove(bin_item)

View File

@@ -35,14 +35,13 @@ class TestSkillProgressionProgressive(SVTestBase):
for level in range(1, 11):
location_name = f"Level {level} {skill}"
location = self.multiworld.get_location(location_name, self.player)
with self.subTest(location_name):
if level > 1:
self.assert_cannot_reach_location(location, self.multiworld.state)
self.assert_cannot_reach_location(location_name)
self.collect(f"{skill} Level")
self.assert_can_reach_location(location, self.multiworld.state)
self.assert_can_reach_location(location_name)
self.reset_collection_state()
@@ -87,8 +86,7 @@ class TestSkillProgressionProgressiveWithMasteryWithoutMods(SVTestBase):
for skill in all_vanilla_skills:
with self.subTest(skill):
location = self.multiworld.get_location(f"{skill} Mastery", self.player)
self.assert_can_reach_location(location, self.multiworld.state)
self.assert_can_reach_location(f"{skill} Mastery")
self.reset_collection_state()
@@ -98,8 +96,7 @@ class TestSkillProgressionProgressiveWithMasteryWithoutMods(SVTestBase):
self.collect_everything()
self.remove_one_by_name(f"{skill} Level")
location = self.multiworld.get_location(f"{skill} Mastery", self.player)
self.assert_cannot_reach_location(location, self.multiworld.state)
self.assert_cannot_reach_location(f"{skill} Mastery")
self.reset_collection_state()
@@ -107,7 +104,6 @@ class TestSkillProgressionProgressiveWithMasteryWithoutMods(SVTestBase):
self.collect_everything()
self.remove_one_by_name(f"Progressive Pickaxe")
location = self.multiworld.get_location("Mining Mastery", self.player)
self.assert_cannot_reach_location(location, self.multiworld.state)
self.assert_cannot_reach_location("Mining Mastery")
self.reset_collection_state()

View File

@@ -18,37 +18,37 @@ class TestProgressiveToolsLogic(SVTestBase):
self.multiworld.state.prog_items = {1: Counter()}
sturgeon_rule = self.world.logic.has("Sturgeon")
self.assert_rule_false(sturgeon_rule, self.multiworld.state)
self.assert_rule_false(sturgeon_rule)
summer = self.create_item("Summer")
self.multiworld.state.collect(summer)
self.assert_rule_false(sturgeon_rule, self.multiworld.state)
self.assert_rule_false(sturgeon_rule)
fishing_rod = self.create_item("Progressive Fishing Rod")
self.multiworld.state.collect(fishing_rod)
self.multiworld.state.collect(fishing_rod)
self.assert_rule_false(sturgeon_rule, self.multiworld.state)
self.assert_rule_false(sturgeon_rule)
fishing_level = self.create_item("Fishing Level")
self.multiworld.state.collect(fishing_level)
self.assert_rule_false(sturgeon_rule, self.multiworld.state)
self.assert_rule_false(sturgeon_rule)
self.multiworld.state.collect(fishing_level)
self.multiworld.state.collect(fishing_level)
self.multiworld.state.collect(fishing_level)
self.multiworld.state.collect(fishing_level)
self.multiworld.state.collect(fishing_level)
self.assert_rule_true(sturgeon_rule, self.multiworld.state)
self.assert_rule_true(sturgeon_rule)
self.remove(summer)
self.assert_rule_false(sturgeon_rule, self.multiworld.state)
self.assert_rule_false(sturgeon_rule)
winter = self.create_item("Winter")
self.multiworld.state.collect(winter)
self.assert_rule_true(sturgeon_rule, self.multiworld.state)
self.assert_rule_true(sturgeon_rule)
self.remove(fishing_rod)
self.assert_rule_false(sturgeon_rule, self.multiworld.state)
self.assert_rule_false(sturgeon_rule)
def test_old_master_cannoli(self):
self.multiworld.state.prog_items = {1: Counter()}
@@ -58,35 +58,34 @@ class TestProgressiveToolsLogic(SVTestBase):
self.multiworld.state.collect(self.create_item("Summer"))
self.collect_lots_of_money()
rule = self.world.logic.region.can_reach_location("Old Master Cannoli")
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location("Old Master Cannoli")
fall = self.create_item("Fall")
self.multiworld.state.collect(fall)
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location("Old Master Cannoli")
tuesday = self.create_item("Traveling Merchant: Tuesday")
self.multiworld.state.collect(tuesday)
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location("Old Master Cannoli")
rare_seed = self.create_item("Rare Seed")
self.multiworld.state.collect(rare_seed)
self.assert_rule_true(rule, self.multiworld.state)
self.assert_can_reach_location("Old Master Cannoli")
self.remove(fall)
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location("Old Master Cannoli")
self.remove(tuesday)
green_house = self.create_item("Greenhouse")
self.multiworld.state.collect(green_house)
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location("Old Master Cannoli")
friday = self.create_item("Traveling Merchant: Friday")
self.multiworld.state.collect(friday)
self.assertTrue(self.multiworld.get_location("Old Master Cannoli", 1).access_rule(self.multiworld.state))
self.assert_can_reach_location("Old Master Cannoli")
self.remove(green_house)
self.assert_rule_false(rule, self.multiworld.state)
self.assert_cannot_reach_location("Old Master Cannoli")
self.remove(friday)
@@ -106,13 +105,13 @@ class TestToolVanillaRequiresBlacksmith(SVTestBase):
for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]:
for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]:
self.assert_rule_false(self.world.logic.tool.has_tool(tool, material), self.multiworld.state)
self.assert_rule_false(self.world.logic.tool.has_tool(tool, material))
self.multiworld.state.collect(self.create_item(railroad_item))
for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]:
for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]:
self.assert_rule_true(self.world.logic.tool.has_tool(tool, material), self.multiworld.state)
self.assert_rule_true(self.world.logic.tool.has_tool(tool, material))
def test_cannot_get_fishing_rod_without_willy_access(self):
railroad_item = "Railroad Boulder Removed"
@@ -120,12 +119,12 @@ class TestToolVanillaRequiresBlacksmith(SVTestBase):
self.collect_all_except(railroad_item)
for fishing_rod_level in [3, 4]:
self.assert_rule_false(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state)
self.assert_rule_false(self.world.logic.tool.has_fishing_rod(fishing_rod_level))
self.multiworld.state.collect(self.create_item(railroad_item))
for fishing_rod_level in [3, 4]:
self.assert_rule_true(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state)
self.assert_rule_true(self.world.logic.tool.has_fishing_rod(fishing_rod_level))
def place_region_at_entrance(multiworld, player, region, entrance):