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): def create_items(self):
self.precollect_starting_season() self.precollect_starting_season()
self.precollect_farm_type_items() self.precollect_building_items()
items_to_exclude = [excluded_items items_to_exclude = [excluded_items
for excluded_items in self.multiworld.precollected_items[self.player] for excluded_items in self.multiworld.precollected_items[self.player]
if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK, if 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)) starting_season = self.create_item(self.random.choice(season_pool))
self.multiworld.push_precollected(starting_season) self.multiworld.push_precollected(starting_season)
def precollect_farm_type_items(self): def precollect_building_items(self):
if self.options.farm_type == FarmType.option_meadowlands and self.options.building_progression & BuildingProgression.option_progressive: building_progression = self.content.features.building_progression
self.multiworld.push_precollected(self.create_item("Progressive Coop")) # 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 setup_logic_events(self):
def register_event(name: str, region: str, rule: StardewRule): def register_event(name: str, region: str, rule: StardewRule):

View File

@@ -1,8 +1,9 @@
from . import content_packs 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 .game_content import ContentPack, StardewContent, StardewFeatures
from .unpacking import unpack_content from .unpacking import unpack_content
from .. import options from .. import options
from ..strings.building_names import Building
def create_content(player_options: options.StardewValleyOptions) -> StardewContent: 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: def choose_features(player_options: options.StardewValleyOptions) -> StardewFeatures:
return StardewFeatures( return StardewFeatures(
choose_booksanity(player_options.booksanity), choose_booksanity(player_options.booksanity),
choose_building_progression(player_options.building_progression, player_options.farm_type),
choose_cropsanity(player_options.cropsanity), choose_cropsanity(player_options.cropsanity),
choose_fishsanity(player_options.fishsanity), choose_fishsanity(player_options.fishsanity),
choose_friendsanity(player_options.friendsanity, player_options.friendsanity_heart_size), 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)}") 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 = { skill_progression_by_option = {
options.SkillProgression.option_vanilla: skill_progression.SkillProgressionVanilla(), options.SkillProgression.option_vanilla: skill_progression.SkillProgressionVanilla(),
options.SkillProgression.option_progressive: skill_progression.SkillProgressionProgressive(), options.SkillProgression.option_progressive: skill_progression.SkillProgressionProgressive(),

View File

@@ -1,4 +1,5 @@
from . import booksanity from . import booksanity
from . import building_progression
from . import cropsanity from . import cropsanity
from . import fishsanity from . import fishsanity
from . import friendsanity 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 dataclasses import dataclass, field
from typing import Dict, Iterable, Set, Any, Mapping, Type, Tuple, Union 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.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.skill import Skill
from ..data.villagers_data import Villager from ..data.villagers_data import Villager
@@ -20,16 +21,17 @@ class StardewContent:
game_items: Dict[str, GameItem] = field(default_factory=dict) game_items: Dict[str, GameItem] = field(default_factory=dict)
fishes: Dict[str, FishItem] = field(default_factory=dict) fishes: Dict[str, FishItem] = field(default_factory=dict)
villagers: Dict[str, Villager] = 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) skills: Dict[str, Skill] = field(default_factory=dict)
quests: Dict[str, Any] = 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 item in self.game_items.values():
for source in item.sources: for source in item.sources:
if isinstance(source, types): if isinstance(source, types):
yield source 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 = self.game_items.setdefault(item_name, GameItem(item_name))
item.add_sources(sources) item.add_sources(sources)
@@ -50,6 +52,7 @@ class StardewContent:
@dataclass(frozen=True) @dataclass(frozen=True)
class StardewFeatures: class StardewFeatures:
booksanity: booksanity.BooksanityFeature booksanity: booksanity.BooksanityFeature
building_progression: building_progression.BuildingProgressionFeature
cropsanity: cropsanity.CropsanityFeature cropsanity: cropsanity.CropsanityFeature
fishsanity: fishsanity.FishsanityFeature fishsanity: fishsanity.FishsanityFeature
friendsanity: friendsanity.FriendsanityFeature friendsanity: friendsanity.FriendsanityFeature
@@ -70,13 +73,13 @@ class ContentPack:
# def item_hook # 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.""" """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): 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): def shop_source_hook(self, content: StardewContent):
... ...
@@ -86,12 +89,12 @@ class ContentPack:
def fish_hook(self, content: StardewContent): 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): 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): def artisan_good_hook(self, content: StardewContent):
... ...
@@ -101,6 +104,11 @@ class ContentPack:
def villager_hook(self, content: StardewContent): def villager_hook(self, content: StardewContent):
... ...
farm_buildings: Iterable[Building] = ()
def farm_building_hook(self, content: StardewContent):
...
skills: Iterable[Skill] = () skills: Iterable[Skill] = ()
def skill_hook(self, content: StardewContent): def skill_hook(self, content: StardewContent):

View File

@@ -1,7 +1,25 @@
from ..game_content import ContentPack from ..game_content import ContentPack
from ..mod_registry import register_mod_content_pack 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 ...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( register_mod_content_pack(ContentPack(
ModNames.tractor, 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 .game_content import StardewContent, ContentPack, StardewFeatures
from .vanilla.base import base_game as base_game_content_pack 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: 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 content.villagers[villager.name] = villager
pack.villager_hook(content) 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: for skill in pack.skills:
content.skills[skill.name] = skill content.skills[skill.name] = skill
pack.skill_hook(content) pack.skill_hook(content)
@@ -73,7 +77,7 @@ def register_pack(content: StardewContent, pack: ContentPack):
def register_sources_and_call_hook(content: StardewContent, 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]): hook: Callable[[StardewContent], None]):
for item_name, sources in sources_by_item_name.items(): for item_name, sources in sources_by_item_name.items():
item = content.game_items.setdefault(item_name, GameItem(item_name)) item = content.game_items.setdefault(item_name, GameItem(item_name))

View File

@@ -1,10 +1,13 @@
from ..game_content import ContentPack from ..game_content import ContentPack
from ...data import villagers_data, fish_data 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.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource
from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement
from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
from ...strings.artisan_good_names import ArtisanGood
from ...strings.book_names import Book from ...strings.book_names import Book
from ...strings.building_names import Building as BuildingNames
from ...strings.crop_names import Fruit from ...strings.crop_names import Fruit
from ...strings.fish_names import WaterItem from ...strings.fish_names import WaterItem
from ...strings.food_names import Beverage, Meal 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.fruit_tree_names import Sapling
from ...strings.generic_names import Generic from ...strings.generic_names import Generic
from ...strings.material_names import Material from ...strings.material_names import Material
from ...strings.metal_names import MetalBar
from ...strings.region_names import Region, LogicRegion from ...strings.region_names import Region, LogicRegion
from ...strings.season_names import Season from ...strings.season_names import Season
from ...strings.seed_names import Seed, TreeSeed from ...strings.seed_names import Seed, TreeSeed
@@ -229,10 +233,10 @@ pelican_town = ContentPack(
ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
Book.mapping_cave_systems: ( Book.mapping_cave_systems: (
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
CompoundSource(sources=(
GenericSource(regions=(Region.adventurer_guild_bedroom,)), 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: ( Book.monster_compendium: (
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
CustomRuleSource(create_rule=lambda logic: logic.monster.can_kill_many(Generic.any)), CustomRuleSource(create_rule=lambda logic: logic.monster.can_kill_many(Generic.any)),
@@ -385,5 +389,204 @@ pelican_town = ContentPack(
villagers_data.vincent, villagers_data.vincent,
villagers_data.willy, villagers_data.willy,
villagers_data.wizard, 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 dataclasses import dataclass
from .game_item import ItemSource from .game_item import Source
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class MachineSource(ItemSource): class MachineSource(Source):
item: str # this should be optional (worm bin) item: str # this should be optional (worm bin)
machine: str machine: str
# seasons # 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) @dataclass(frozen=True)
class ItemSource(ABC): class Source(ABC):
add_tags: ClassVar[Tuple[ItemTag]] = () add_tags: ClassVar[Tuple[ItemTag]] = ()
other_requirements: Tuple[Requirement, ...] = field(kw_only=True, default_factory=tuple) other_requirements: Tuple[Requirement, ...] = field(kw_only=True, default_factory=tuple)
@@ -38,23 +38,18 @@ class ItemSource(ABC):
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class GenericSource(ItemSource): class GenericSource(Source):
regions: Tuple[str, ...] = () regions: Tuple[str, ...] = ()
"""No region means it's available everywhere.""" """No region means it's available everywhere."""
@dataclass(frozen=True) @dataclass(frozen=True)
class CustomRuleSource(ItemSource): class CustomRuleSource(Source):
"""Hopefully once everything is migrated to sources, we won't need these custom logic anymore.""" """Hopefully once everything is migrated to sources, we won't need these custom logic anymore."""
create_rule: Callable[[Any], StardewRule] create_rule: Callable[[Any], StardewRule]
@dataclass(frozen=True, kw_only=True) class Tag(Source):
class CompoundSource(ItemSource):
sources: Tuple[ItemSource, ...] = ()
class Tag(ItemSource):
"""Not a real source, just a way to add tags to an item. Will be removed from the item sources during unpacking.""" """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, ...] tag: Tuple[ItemTag, ...]
@@ -69,10 +64,10 @@ class Tag(ItemSource):
@dataclass(frozen=True) @dataclass(frozen=True)
class GameItem: class GameItem:
name: str name: str
sources: List[ItemSource] = field(default_factory=list) sources: List[Source] = field(default_factory=list)
tags: Set[ItemTag] = field(default_factory=set) 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) self.sources.extend(source for source in sources if type(source) is not Tag)
for source in sources: for source in sources:
self.add_tags(source.add_tags) self.add_tags(source.add_tags)

View File

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

View File

@@ -509,6 +509,7 @@ id,name,classification,groups,mod_name
561,Fishing Bar Size Bonus,filler,PLAYER_BUFF, 561,Fishing Bar Size Bonus,filler,PLAYER_BUFF,
562,Quality Bonus,filler,PLAYER_BUFF, 562,Quality Bonus,filler,PLAYER_BUFF,
563,Glow Bonus,filler,PLAYER_BUFF, 563,Glow Bonus,filler,PLAYER_BUFF,
564,Pet Bowl,progression,BUILDING,
4001,Burnt Trap,trap,TRAP, 4001,Burnt Trap,trap,TRAP,
4002,Darkness Trap,trap,TRAP, 4002,Darkness Trap,trap,TRAP,
4003,Frozen 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 level: int
@dataclass(frozen=True)
class RegionRequirement(Requirement):
region: str
@dataclass(frozen=True) @dataclass(frozen=True)
class SeasonRequirement(Requirement): class SeasonRequirement(Requirement):
season: str season: str

View File

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

View File

@@ -23,9 +23,9 @@ def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions,
add_seasonal_candidates(early_candidates, options) 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) 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 Coop")
early_candidates.append("Progressive Barn") 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 .logic.logic_event import all_events
from .mods.mod_data import ModNames from .mods.mod_data import ModNames
from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \ 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 Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs
from .strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName from .strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName
from .strings.ap_names.ap_weapon_names import APWeapon 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_tools(item_factory, content, items)
create_skills(item_factory, content, items) create_skills(item_factory, content, items)
create_wizard_buildings(item_factory, options, 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("Railroad Boulder Removed"))
items.append(item_factory(CommunityUpgrade.fruit_bats)) items.append(item_factory(CommunityUpgrade.fruit_bats))
items.append(item_factory(CommunityUpgrade.mushroom_boxes)) 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")) items.append(item_factory("Woods Obelisk"))
def create_carpenter_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): def create_carpenter_buildings(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]):
building_option = options.building_progression building_progression = content.features.building_progression
if not building_option & BuildingProgression.option_progressive: if not building_progression.is_progressive:
return return
items.append(item_factory("Progressive Coop"))
items.append(item_factory("Progressive Coop")) for building in content.farm_buildings.values():
items.append(item_factory("Progressive Coop")) item_name, _ = building_progression.to_progressive_item(building.name)
items.append(item_factory("Progressive Barn")) items.append(item_factory(item_name))
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"))
def create_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): 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 .data.museum_data import all_museum_items
from .mods.mod_data import ModNames from .mods.mod_data import ModNames
from .options import ExcludeGingerIsland, ArcadeMachineLocations, SpecialOrderLocations, Museumsanity, \ 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 .options import StardewValleyOptions, Craftsanity, Chefsanity, Cooksanity, Shipsanity, Monstersanity
from .strings.goal_names import Goal from .strings.goal_names import Goal
from .strings.quest_names import ModQuest, Quest from .strings.quest_names import ModQuest, Quest
@@ -261,6 +261,19 @@ def extend_baby_locations(randomized_locations: List[LocationData]):
randomized_locations.extend(baby_locations) 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): def extend_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random):
if options.festival_locations == FestivalLocations.option_disabled: if options.festival_locations == FestivalLocations.option_disabled:
return return
@@ -485,10 +498,7 @@ def create_locations(location_collector: StardewLocationCollector,
if skill_progression.is_mastery_randomized(skill): if skill_progression.is_mastery_randomized(skill):
randomized_locations.append(location_table[skill.mastery_name]) randomized_locations.append(location_table[skill.mastery_name])
if options.building_progression & BuildingProgression.option_progressive: extend_building_locations(randomized_locations, content)
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])
if options.arcade_machine_locations != ArcadeMachineLocations.option_disabled: if options.arcade_machine_locations != ArcadeMachineLocations.option_disabled:
randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE_VICTORY]) 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.museum_rules: Dict[str, StardewRule] = {}
self.festival_rules: Dict[str, StardewRule] = {} self.festival_rules: Dict[str, StardewRule] = {}
self.quest_rules: Dict[str, StardewRule] = {} self.quest_rules: Dict[str, StardewRule] = {}
self.building_rules: Dict[str, StardewRule] = {}
self.special_order_rules: Dict[str, StardewRule] = {} self.special_order_rules: Dict[str, StardewRule] = {}
self.sve_location_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 functools import cached_property
from typing import Dict, Union from typing import Union
from Utils import cache_self1 from Utils import cache_self1
from .base_logic import BaseLogic, BaseLogicMixin from .base_logic import BaseLogic, BaseLogicMixin
from .has_logic import HasLogicMixin from .has_logic import HasLogicMixin
from .money_logic import MoneyLogicMixin
from .received_logic import ReceivedLogicMixin from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin from .region_logic import RegionLogicMixin
from ..options import BuildingProgression from ..stardew_rule import StardewRule, true_
from ..stardew_rule import StardewRule, True_, False_, Has
from ..strings.artisan_good_names import ArtisanGood
from ..strings.building_names import Building 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 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): class BuildingLogicMixin(BaseLogicMixin):
@@ -25,78 +25,38 @@ class BuildingLogicMixin(BaseLogicMixin):
self.building = BuildingLogic(*args, **kwargs) self.building = BuildingLogic(*args, **kwargs)
class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, MoneyLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin]]): class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SourceLogicMixin]]):
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)
@cache_self1 @cache_self1
def has_building(self, building: str) -> StardewRule: def can_build(self, building_name: str) -> StardewRule:
# Shipping bin is special. The mod auto-builds it when received, no need to go to Robin. building = self.content.farm_buildings.get(building_name)
if building is Building.shipping_bin: assert building is not None, f"Building {building_name} not found."
if not self.options.building_progression & BuildingProgression.option_progressive:
return True_() source_rule = self.logic.source.has_access_to_any(building.sources)
return self.logic.received(building) 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 carpenter_rule = self.logic.building.can_construct_buildings
if not self.options.building_progression & BuildingProgression.option_progressive: item, count = building_progression.to_progressive_item(building_name)
return Has(building, self.registry.building_rules, has_group) & carpenter_rule return self.logic.received(item, count) & 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
@cached_property @cached_property
def can_construct_buildings(self) -> StardewRule: def can_construct_buildings(self) -> StardewRule:
return self.logic.region.can_reach(Region.carpenter) 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 ..data.recipe_source import CutsceneSource, ShopTradeSource
from ..options import Chefsanity from ..options import Chefsanity
from ..stardew_rule import StardewRule, True_, False_ from ..stardew_rule import StardewRule, True_, False_
from ..strings.building_names import Building
from ..strings.region_names import LogicRegion from ..strings.region_names import LogicRegion
from ..strings.skill_names import Skill from ..strings.skill_names import Skill
from ..strings.tv_channel_names import Channel from ..strings.tv_channel_names import Channel
@@ -32,7 +33,7 @@ class CookingLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogi
BuildingLogicMixin, RelationshipLogicMixin, SkillLogicMixin, CookingLogicMixin]]): BuildingLogicMixin, RelationshipLogicMixin, SkillLogicMixin, CookingLogicMixin]]):
@cached_property @cached_property
def can_cook_in_kitchen(self) -> StardewRule: 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 # Should be cached
def can_cook(self, recipe: CookingRecipe = None) -> StardewRule: def can_cook(self, recipe: CookingRecipe = None) -> StardewRule:

View File

@@ -44,7 +44,7 @@ class GoalLogic(BaseLogic[StardewLogic]):
self.logic.museum.can_complete_museum(), self.logic.museum.can_complete_museum(),
# Catching every fish not expected # Catching every fish not expected
# Shipping every item 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(5, 8), # 5 Friends
self.logic.relationship.has_hearts_with_n(10, 8), # 10 friends self.logic.relationship.has_hearts_with_n(10, 8), # 10 friends
self.logic.pet.has_pet_hearts(5), # Max Pet 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.currency_names import Currency
from ..strings.fish_names import WaterChest from ..strings.fish_names import WaterChest
from ..strings.geode_names import Geode from ..strings.geode_names import Geode
from ..strings.material_names import Material
from ..strings.region_names import Region from ..strings.region_names import Region
from ..strings.tool_names import Tool from ..strings.tool_names import Tool
@@ -21,9 +22,14 @@ if TYPE_CHECKING:
else: else:
ToolLogicMixin = object ToolLogicMixin = object
MIN_ITEMS = 10 MIN_MEDIUM_ITEMS = 10
MAX_ITEMS = 999 MAX_MEDIUM_ITEMS = 999
PERCENT_REQUIRED_FOR_MAX_ITEM = 24 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): 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. # 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) time_rule = self.logic.time.has_lived_months(quantity // 14)
return self.logic.and_(opening_rule, mystery_box_rule, 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: def can_grind_artifact_troves(self, quantity: int) -> StardewRule:
opening_rule = self.logic.region.can_reach(Region.blacksmith) 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. # Assuming twelve per month if the player does not grind it.
self.logic.time.has_lived_months(quantity // 12)) 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 @cache_self1
def can_grind_item(self, quantity: int) -> StardewRule: def can_grind_medium_item(self, quantity: int) -> StardewRule:
if quantity <= MIN_ITEMS: if quantity <= MIN_MEDIUM_ITEMS:
return self.logic.true_ return self.logic.true_
quantity = min(quantity, MAX_ITEMS) quantity = min(quantity, MAX_MEDIUM_ITEMS)
price = max(1, quantity * PERCENT_REQUIRED_FOR_MAX_ITEM // MAX_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) 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), 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.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.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.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.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, 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_() 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.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.initialize_rules()
self.quest.update_rules(self.mod.quest.get_modded_quest_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: if typing.TYPE_CHECKING:
from .shipping_logic import ShippingLogicMixin from .shipping_logic import ShippingLogicMixin
else:
assert ShippingLogicMixin ShippingLogicMixin = object
qi_gem_rewards = ("100 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems", "25 Qi Gems", 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") "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, class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SeasonLogicMixin,
GrindLogicMixin, 'ShippingLogicMixin']]): GrindLogicMixin, ShippingLogicMixin]]):
@cache_self1 @cache_self1
def can_have_earned_total(self, amount: int) -> StardewRule: def can_have_earned_total(self, amount: int) -> StardewRule:
@@ -80,7 +80,7 @@ GrindLogicMixin, 'ShippingLogicMixin']]):
item_rules = [] item_rules = []
if source.items_price is not None: if source.items_price is not None:
for price, item in source.items_price: 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) 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 ..data.villagers_data import Villager
from ..stardew_rule import StardewRule, True_, false_, true_ from ..stardew_rule import StardewRule, True_, false_, true_
from ..strings.ap_names.mods.mod_items import SVEQuestItem from ..strings.ap_names.mods.mod_items import SVEQuestItem
from ..strings.building_names import Building
from ..strings.generic_names import Generic from ..strings.generic_names import Generic
from ..strings.gift_names import Gift from ..strings.gift_names import Gift
from ..strings.quest_names import ModQuest
from ..strings.region_names import Region from ..strings.region_names import Region
from ..strings.season_names import Season from ..strings.season_names import Season
from ..strings.villager_names import NPC, ModNPC from ..strings.villager_names import NPC, ModNPC
@@ -63,7 +63,7 @@ ReceivedLogicMixin, HasLogicMixin, ModLogicMixin]]):
if not self.content.features.friendsanity.is_enabled: if not self.content.features.friendsanity.is_enabled:
return self.logic.relationship.can_reproduce(number_children) 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: def can_reproduce(self, number_children: int = 1) -> StardewRule:
assert number_children >= 0, "Can't have a negative amount of children." assert number_children >= 0, "Can't have a negative amount of children."
@@ -71,7 +71,7 @@ ReceivedLogicMixin, HasLogicMixin, ModLogicMixin]]):
return True_() return True_()
baby_rules = [self.logic.relationship.can_get_married(), 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_hearts_with_any_bachelor(12),
self.logic.relationship.has_children(number_children - 1)] 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 .has_logic import HasLogicMixin
from .quest_logic import QuestLogicMixin from .quest_logic import QuestLogicMixin
from .received_logic import ReceivedLogicMixin from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .relationship_logic import RelationshipLogicMixin from .relationship_logic import RelationshipLogicMixin
from .season_logic import SeasonLogicMixin from .season_logic import SeasonLogicMixin
from .skill_logic import SkillLogicMixin from .skill_logic import SkillLogicMixin
@@ -16,7 +17,7 @@ from .tool_logic import ToolLogicMixin
from .walnut_logic import WalnutLogicMixin from .walnut_logic import WalnutLogicMixin
from ..data.game_item import Requirement from ..data.game_item import Requirement
from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, CombatRequirement, QuestRequirement, \ from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, CombatRequirement, QuestRequirement, \
RelationshipRequirement, FishingRequirement, WalnutRequirement RelationshipRequirement, FishingRequirement, WalnutRequirement, RegionRequirement
class RequirementLogicMixin(BaseLogicMixin): class RequirementLogicMixin(BaseLogicMixin):
@@ -26,7 +27,7 @@ class RequirementLogicMixin(BaseLogicMixin):
class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin, 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]): def meet_all_requirements(self, requirements: Iterable[Requirement]):
if not requirements: if not requirements:
@@ -45,6 +46,10 @@ SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, Relationshi
def _(self, requirement: SkillRequirement): def _(self, requirement: SkillRequirement):
return self.logic.skill.has_level(requirement.skill, requirement.level) 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 @meet_requirement.register
def _(self, requirement: BookRequirement): def _(self, requirement: BookRequirement):
return self.logic.book.has_book_power(requirement.book) return self.logic.book.has_book_power(requirement.book)
@@ -76,5 +81,3 @@ SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, Relationshi
@meet_requirement.register @meet_requirement.register
def _(self, requirement: FishingRequirement): def _(self, requirement: FishingRequirement):
return self.logic.fishing.can_fish_at(requirement.region) 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 .requirement_logic import RequirementLogicMixin
from .tool_logic import ToolLogicMixin from .tool_logic import ToolLogicMixin
from ..data.artisan import MachineSource 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, \ from ..data.harvest import ForagingSource, FruitBatsSource, MushroomCaveSource, SeasonalForagingSource, \
HarvestCropSource, HarvestFruitTreeSource, ArtifactSpotSource HarvestCropSource, HarvestFruitTreeSource, ArtifactSpotSource
from ..data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource 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, 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): def has_access_to_item(self, item: GameItem):
rules = [] rules = []
@@ -36,14 +36,10 @@ class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogic
rules.append(self.logic.source.has_access_to_any(item.sources)) rules.append(self.logic.source.has_access_to_any(item.sources))
return self.logic.and_(*rules) 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) return self.logic.or_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements)
for source in sources)) 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 @functools.singledispatchmethod
def has_access_to(self, source: Any): def has_access_to(self, source: Any):
raise ValueError(f"Sources of type{type(source)} have no rule registered.") 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): def _(self, source: CustomRuleSource):
return source.create_rule(self.logic) 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 @has_access_to.register
def _(self, source: ForagingSource): def _(self, source: ForagingSource):
return self.logic.harvesting.can_forage_from(source) 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 .deepwoods_logic import DeepWoodsLogicMixin
from .elevator_logic import ModElevatorLogicMixin from .elevator_logic import ModElevatorLogicMixin
from .item_logic import ModItemLogicMixin from .item_logic import ModItemLogicMixin
@@ -16,6 +15,6 @@ class ModLogicMixin(BaseLogicMixin):
self.mod = ModLogic(*args, **kwargs) self.mod = ModLogic(*args, **kwargs)
class ModLogic(ModElevatorLogicMixin, MagicLogicMixin, ModSkillLogicMixin, ModItemLogicMixin, ModQuestLogicMixin, ModBuildingLogicMixin, class ModLogic(ModElevatorLogicMixin, MagicLogicMixin, ModSkillLogicMixin, ModItemLogicMixin, ModQuestLogicMixin,
ModSpecialOrderLogicMixin, DeepWoodsLogicMixin, SVELogicMixin): ModSpecialOrderLogicMixin, DeepWoodsLogicMixin, SVELogicMixin):
pass pass

View File

@@ -19,7 +19,7 @@ from .logic.logic import StardewLogic
from .logic.time_logic import MAX_MONTHS from .logic.time_logic import MAX_MONTHS
from .logic.tool_logic import tool_upgrade_prices from .logic.tool_logic import tool_upgrade_prices
from .mods.mod_data import ModNames 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 Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity, StardewValleyOptions, Walnutsanity
from .stardew_rule import And, StardewRule, true_ from .stardew_rule import And, StardewRule, true_
from .stardew_rule.indirect_connection import look_for_indirect_connection 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_tool_rules(logic, multiworld, player, world_content)
set_skills_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_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_cropsanity_rules(logic, multiworld, player, world_content)
set_story_quests_rules(all_location_names, logic, multiworld, player, world_options) set_story_quests_rules(all_location_names, logic, multiworld, player, world_options)
set_special_order_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)) MultiWorldRules.set_rule(tool_upgrade_location, logic.tool.has_tool(tool, previous))
def set_building_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): def set_building_rules(logic: StardewLogic, multiworld, player, content: StardewContent):
if not world_options.building_progression & BuildingProgression.option_progressive: building_progression = content.features.building_progression
if not building_progression.is_progressive:
return return
for building in locations.locations_by_tag[LocationTags.BUILDING_BLUEPRINT]: for building in content.farm_buildings.values():
if building.mod_name is not None and building.mod_name not in world_options.mods: if building.name in building_progression.starting_buildings:
continue 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): 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): 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.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_greenhouse, logic.received("Greenhouse"))
set_entrance_rule(multiworld, player, Entrance.enter_coop, logic.building.has_building(Building.coop)) set_entrance_rule(multiworld, player, Entrance.enter_coop, logic.building.has_building(Building.coop))

View File

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

View File

@@ -61,11 +61,13 @@ class TestBooksanityNone(SVTestBase):
for location in self.multiworld.get_locations(): for location in self.multiworld.get_locations():
if not location.name.startswith(shipsanity_prefix): if not location.name.startswith(shipsanity_prefix):
continue continue
item_to_ship = location.name[len(shipsanity_prefix):] item_to_ship = location.name[len(shipsanity_prefix):]
if item_to_ship not in power_books and item_to_ship not in skill_books: if item_to_ship not in power_books and item_to_ship not in skill_books:
continue continue
with self.subTest(location.name): with self.subTest(location.name):
self.assert_can_reach_location(location, self.multiworld.state) self.assert_can_reach_location(location)
class TestBooksanityPowers(SVTestBase): class TestBooksanityPowers(SVTestBase):
@@ -107,11 +109,13 @@ class TestBooksanityPowers(SVTestBase):
for location in self.multiworld.get_locations(): for location in self.multiworld.get_locations():
if not location.name.startswith(shipsanity_prefix): if not location.name.startswith(shipsanity_prefix):
continue continue
item_to_ship = location.name[len(shipsanity_prefix):] item_to_ship = location.name[len(shipsanity_prefix):]
if item_to_ship not in power_books and item_to_ship not in skill_books: if item_to_ship not in power_books and item_to_ship not in skill_books:
continue continue
with self.subTest(location.name): with self.subTest(location.name):
self.assert_can_reach_location(location, self.multiworld.state) self.assert_can_reach_location(location)
class TestBooksanityPowersAndSkills(SVTestBase): class TestBooksanityPowersAndSkills(SVTestBase):
@@ -153,11 +157,13 @@ class TestBooksanityPowersAndSkills(SVTestBase):
for location in self.multiworld.get_locations(): for location in self.multiworld.get_locations():
if not location.name.startswith(shipsanity_prefix): if not location.name.startswith(shipsanity_prefix):
continue continue
item_to_ship = location.name[len(shipsanity_prefix):] item_to_ship = location.name[len(shipsanity_prefix):]
if item_to_ship not in power_books and item_to_ship not in skill_books: if item_to_ship not in power_books and item_to_ship not in skill_books:
continue continue
with self.subTest(location.name): with self.subTest(location.name):
self.assert_can_reach_location(location, self.multiworld.state) self.assert_can_reach_location(location)
class TestBooksanityAll(SVTestBase): class TestBooksanityAll(SVTestBase):
@@ -199,8 +205,10 @@ class TestBooksanityAll(SVTestBase):
for location in self.multiworld.get_locations(): for location in self.multiworld.get_locations():
if not location.name.startswith(shipsanity_prefix): if not location.name.startswith(shipsanity_prefix):
continue continue
item_to_ship = location.name[len(shipsanity_prefix):] item_to_ship = location.name[len(shipsanity_prefix):]
if item_to_ship not in power_books and item_to_ship not in skill_books: if item_to_ship not in power_books and item_to_ship not in skill_books:
continue continue
with self.subTest(location.name): 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 SVTestBase
from .. import options 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): class TestCropsanityRules(SVTestBase):
@@ -8,13 +12,13 @@ class TestCropsanityRules(SVTestBase):
} }
def test_need_greenhouse_for_cactus(self): def test_need_greenhouse_for_cactus(self):
harvest_cactus = self.world.logic.region.can_reach_location("Harvest Cactus Fruit") harvest_cactus_fruit = "Harvest Cactus Fruit"
self.assert_rule_false(harvest_cactus, self.multiworld.state) self.assert_cannot_reach_location(harvest_cactus_fruit)
self.multiworld.state.collect(self.create_item("Cactus Seeds")) self.multiworld.state.collect(self.create_item(Seed.cactus))
self.multiworld.state.collect(self.create_item("Shipping Bin")) self.multiworld.state.collect(self.create_item(Building.shipping_bin))
self.multiworld.state.collect(self.create_item("Desert Obelisk")) self.multiworld.state.collect(self.create_item(Transportation.desert_obelisk))
self.assert_rule_false(harvest_cactus, self.multiworld.state) self.assert_cannot_reach_location(harvest_cactus_fruit)
self.multiworld.state.collect(self.create_item("Greenhouse")) self.multiworld.state.collect(self.create_item(Region.greenhouse))
self.assert_rule_true(harvest_cactus, self.multiworld.state) self.assert_can_reach_location(harvest_cactus_fruit)

View File

@@ -1,3 +1,5 @@
from collections import Counter
from . import SVTestBase from . import SVTestBase
from .assertion import WorldAssertMixin from .assertion import WorldAssertMixin
from .. import options from .. import options
@@ -5,27 +7,49 @@ from .. import options
class TestStartInventoryStandardFarm(WorldAssertMixin, SVTestBase): class TestStartInventoryStandardFarm(WorldAssertMixin, SVTestBase):
options = { options = {
options.FarmType.internal_name: options.FarmType.option_standard, options.FarmType: options.FarmType.option_standard,
} }
def test_start_inventory_progressive_coops(self): 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])) start_items = Counter((i.name for i in self.multiworld.precollected_items[self.player]))
items = dict(map(lambda x: (x.name, self.multiworld.itempool.count(x)), self.multiworld.itempool)) items = Counter((i.name for i in self.multiworld.itempool))
self.assertIn("Progressive Coop", items) self.assertIn("Progressive Coop", items)
self.assertEqual(items["Progressive Coop"], 3) self.assertEqual(items["Progressive Coop"], 3)
self.assertNotIn("Progressive Coop", start_items) 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 = {
options.FarmType.internal_name: options.FarmType.option_meadowlands, options.FarmType: options.FarmType.option_meadowlands,
options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, options.BuildingProgression: options.BuildingProgression.option_progressive,
} }
def test_start_inventory_progressive_coops(self): 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])) start_items = Counter((i.name for i in self.multiworld.precollected_items[self.player]))
items = dict(map(lambda x: (x.name, self.multiworld.itempool.count(x)), self.multiworld.itempool)) items = Counter((i.name for i in self.multiworld.itempool))
self.assertIn("Progressive Coop", items) self.assertIn("Progressive Coop", items)
self.assertEqual(items["Progressive Coop"], 2) self.assertEqual(items["Progressive Coop"], 2)
self.assertIn("Progressive Coop", start_items) self.assertIn("Progressive Coop", start_items)
self.assertEqual(start_items["Progressive Coop"], 1) 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 BaseClasses import ItemClassification, Item
from . import SVTestBase from . import SVTestBase
from .. import items, location_table, options from .. import items, location_table, options
from ..items import Group from ..items import Group, ItemData
from ..locations import LocationTags from ..locations import LocationTags
from ..options import Friendsanity, SpecialOrderLocations, Shipsanity, Chefsanity, SeasonRandomization, Craftsanity, ExcludeGingerIsland, SkillProgression, \ from ..options import Friendsanity, SpecialOrderLocations, Shipsanity, Chefsanity, SeasonRandomization, Craftsanity, ExcludeGingerIsland, SkillProgression, \
Booksanity, Walnutsanity Booksanity, Walnutsanity
from ..strings.region_names import Region 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): class TestBaseItemGeneration(SVTestBase):
options = { options = {
SeasonRandomization.internal_name: SeasonRandomization.option_progressive, SeasonRandomization.internal_name: SeasonRandomization.option_progressive,
@@ -25,17 +42,8 @@ class TestBaseItemGeneration(SVTestBase):
} }
def test_all_progression_items_are_added_to_the_pool(self): def test_all_progression_items_are_added_to_the_pool(self):
all_created_items = [item.name for item in self.multiworld.itempool] all_created_items = set(self.get_all_created_items())
# Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression progression_items = get_all_permanent_progression_items()
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]
for progression_item in progression_items: for progression_item in progression_items:
with self.subTest(f"{progression_item.name}"): with self.subTest(f"{progression_item.name}"):
self.assertIn(progression_item.name, all_created_items) 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)) self.assertEqual(len(non_event_locations), len(self.multiworld.itempool))
def test_does_not_create_deprecated_items(self): 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]: for deprecated_item in items.items_by_group[items.Group.DEPRECATED]:
with self.subTest(f"{deprecated_item.name}"): with self.subTest(f"{deprecated_item.name}"):
self.assertNotIn(deprecated_item.name, all_created_items) self.assertNotIn(deprecated_item.name, all_created_items)
def test_does_not_create_more_than_one_maximum_one_items(self): 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]: for maximum_one_item in items.items_by_group[items.Group.MAXIMUM_ONE]:
with self.subTest(f"{maximum_one_item.name}"): with self.subTest(f"{maximum_one_item.name}"):
self.assertLessEqual(all_created_items.count(maximum_one_item.name), 1) self.assertLessEqual(all_created_items.count(maximum_one_item.name), 1)
def test_does_not_create_exactly_two_items(self): def test_does_not_create_or_create_two_of_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]: for exactly_two_item in items.items_by_group[items.Group.EXACTLY_TWO]:
with self.subTest(f"{exactly_two_item.name}"): with self.subTest(f"{exactly_two_item.name}"):
count = all_created_items.count(exactly_two_item.name) count = all_created_items.count(exactly_two_item.name)
@@ -77,17 +85,10 @@ class TestNoGingerIslandItemGeneration(SVTestBase):
} }
def test_all_progression_items_except_island_are_added_to_the_pool(self): def test_all_progression_items_except_island_are_added_to_the_pool(self):
all_created_items = [item.name for item in self.multiworld.itempool] all_created_items = set(self.get_all_created_items())
# Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression progression_items = get_all_permanent_progression_items()
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]
for progression_item in progression_items: for progression_item in progression_items:
with self.subTest(f"{progression_item.name}"): with self.subTest(f"{progression_item.name}"):
if Group.GINGER_ISLAND in progression_item.groups: if Group.GINGER_ISLAND in progression_item.groups:
self.assertNotIn(progression_item.name, all_created_items) 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)) self.assertEqual(len(non_event_locations), len(self.multiworld.itempool))
def test_does_not_create_deprecated_items(self): 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]: for deprecated_item in items.items_by_group[items.Group.DEPRECATED]:
with self.subTest(f"Deprecated item: {deprecated_item.name}"): with self.subTest(f"Deprecated item: {deprecated_item.name}"):
self.assertNotIn(deprecated_item.name, all_created_items) self.assertNotIn(deprecated_item.name, all_created_items)
def test_does_not_create_more_than_one_maximum_one_items(self): 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]: for maximum_one_item in items.items_by_group[items.Group.MAXIMUM_ONE]:
with self.subTest(f"{maximum_one_item.name}"): with self.subTest(f"{maximum_one_item.name}"):
self.assertLessEqual(all_created_items.count(maximum_one_item.name), 1) self.assertLessEqual(all_created_items.count(maximum_one_item.name), 1)
def test_does_not_create_exactly_two_items(self): 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]: for exactly_two_item in items.items_by_group[items.Group.EXACTLY_TWO]:
with self.subTest(f"{exactly_two_item.name}"): with self.subTest(f"{exactly_two_item.name}"):
count = all_created_items.count(exactly_two_item.name) count = all_created_items.count(exactly_two_item.name)

View File

@@ -49,9 +49,9 @@ class LogicTestBase(RuleAssertMixin, TestCase):
self.assert_rule_can_be_resolved(rule, self.multiworld.state) self.assert_rule_can_be_resolved(rule, self.multiworld.state)
def test_given_building_rule_then_can_be_resolved(self): 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): 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) self.assert_rule_can_be_resolved(rule, self.multiworld.state)
def test_given_quest_rule_then_can_be_resolved(self): 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} BuildingProgression.internal_name: BuildingProgression.option_vanilla}
def test_options_are_not_detected_as_progressive(self): def test_options_are_not_detected_as_progressive(self):
world_options = self.world.options
tool_progressive = self.world.content.features.tool_progression.is_progressive 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(tool_progressive)
self.assertFalse(building_progressive) self.assertFalse(building_progressive)
@@ -25,9 +24,8 @@ class TestBitFlagsVanillaCheap(SVTestBase):
BuildingProgression.internal_name: BuildingProgression.option_vanilla_cheap} BuildingProgression.internal_name: BuildingProgression.option_vanilla_cheap}
def test_options_are_not_detected_as_progressive(self): def test_options_are_not_detected_as_progressive(self):
world_options = self.world.options
tool_progressive = self.world.content.features.tool_progression.is_progressive 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(tool_progressive)
self.assertFalse(building_progressive) self.assertFalse(building_progressive)
@@ -42,9 +40,8 @@ class TestBitFlagsVanillaVeryCheap(SVTestBase):
BuildingProgression.internal_name: BuildingProgression.option_vanilla_very_cheap} BuildingProgression.internal_name: BuildingProgression.option_vanilla_very_cheap}
def test_options_are_not_detected_as_progressive(self): def test_options_are_not_detected_as_progressive(self):
world_options = self.world.options
tool_progressive = self.world.content.features.tool_progression.is_progressive 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(tool_progressive)
self.assertFalse(building_progressive) self.assertFalse(building_progressive)
@@ -59,9 +56,8 @@ class TestBitFlagsProgressive(SVTestBase):
BuildingProgression.internal_name: BuildingProgression.option_progressive} BuildingProgression.internal_name: BuildingProgression.option_progressive}
def test_options_are_detected_as_progressive(self): def test_options_are_detected_as_progressive(self):
world_options = self.world.options
tool_progressive = self.world.content.features.tool_progression.is_progressive 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(tool_progressive)
self.assertTrue(building_progressive) self.assertTrue(building_progressive)
@@ -76,9 +72,8 @@ class TestBitFlagsProgressiveCheap(SVTestBase):
BuildingProgression.internal_name: BuildingProgression.option_progressive_cheap} BuildingProgression.internal_name: BuildingProgression.option_progressive_cheap}
def test_options_are_detected_as_progressive(self): def test_options_are_detected_as_progressive(self):
world_options = self.world.options
tool_progressive = self.world.content.features.tool_progression.is_progressive 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(tool_progressive)
self.assertTrue(building_progressive) self.assertTrue(building_progressive)
@@ -93,9 +88,8 @@ class TestBitFlagsProgressiveVeryCheap(SVTestBase):
BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap} BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap}
def test_options_are_detected_as_progressive(self): def test_options_are_detected_as_progressive(self):
world_options = self.world.options
tool_progressive = self.world.content.features.tool_progression.is_progressive 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(tool_progressive)
self.assertTrue(building_progressive) self.assertTrue(building_progressive)

View File

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

View File

@@ -1,3 +1,4 @@
import itertools
import logging import logging
import os import os
import threading 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 worlds.AutoWorld import call_all
from .assertion import RuleAssertMixin from .assertion import RuleAssertMixin
from .options.utils import fill_namespace_with_default, parse_class_option_keys, fill_dataclass_with_default 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 from ..options import StardewValleyOption
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -96,6 +98,12 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
return False return False
return super().run_default_tests 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): def collect_lots_of_money(self, percent: float = 0.25):
self.collect("Shipping Bin") self.collect("Shipping Bin")
real_total_prog_items = self.world.total_progression_items 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: def create_item(self, item: str) -> StardewItem:
return self.world.create_item(item) 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: def remove_one_by_name(self, item: str) -> None:
self.remove(self.create_item(item)) self.remove(self.create_item(item))
def reset_collection_state(self): def reset_collection_state(self) -> None:
self.multiworld.state = self.original_state.copy() 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 = {} pre_generated_worlds = {}

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
import random import random
from BaseClasses import get_seed, ItemClassification from BaseClasses import get_seed
from .. import SVTestBase, SVTestCase from .. import SVTestBase, SVTestCase
from ..TestGeneration import get_all_permanent_progression_items
from ..assertion import ModAssertMixin, WorldAssertMixin from ..assertion import ModAssertMixin, WorldAssertMixin
from ..options.presets import allsanity_mods_6_x_x from ..options.presets import allsanity_mods_6_x_x
from ..options.utils import fill_dataclass_with_default 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 ...mods.mod_data import ModNames
from ...options import SkillProgression, Walnutsanity from ...options import SkillProgression, Walnutsanity
from ...options.options import all_mods from ...options.options import all_mods
@@ -109,17 +110,8 @@ class TestBaseItemGeneration(SVTestBase):
} }
def test_all_progression_items_are_added_to_the_pool(self): def test_all_progression_items_are_added_to_the_pool(self):
all_created_items = [item.name for item in self.multiworld.itempool] all_created_items = self.get_all_created_items()
# Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression progression_items = get_all_permanent_progression_items()
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]
for progression_item in progression_items: for progression_item in progression_items:
with self.subTest(f"{progression_item.name}"): with self.subTest(f"{progression_item.name}"):
self.assertIn(progression_item.name, all_created_items) 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): def test_all_progression_items_except_island_are_added_to_the_pool(self):
all_created_items = [item.name for item in self.multiworld.itempool] all_created_items = self.get_all_created_items()
# Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression progression_items = get_all_permanent_progression_items()
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]
for progression_item in progression_items: for progression_item in progression_items:
with self.subTest(f"{progression_item.name}"): with self.subTest(f"{progression_item.name}"):
if Group.GINGER_ISLAND in progression_item.groups: 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.ElevatorProgression.internal_name: options.ElevatorProgression.default,
options.EntranceRandomization.internal_name: options.EntranceRandomization.default, options.EntranceRandomization.internal_name: options.EntranceRandomization.default,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.default, options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.default,
options.FarmType.internal_name: options.FarmType.default,
options.FestivalLocations.internal_name: options.FestivalLocations.default, options.FestivalLocations.internal_name: options.FestivalLocations.default,
options.Fishsanity.internal_name: options.Fishsanity.default, options.Fishsanity.internal_name: options.Fishsanity.default,
options.Friendsanity.internal_name: options.Friendsanity.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.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive,
options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, 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.FestivalLocations.internal_name: options.FestivalLocations.option_hard,
options.Fishsanity.internal_name: options.Fishsanity.option_all, options.Fishsanity.internal_name: options.Fishsanity.option_all,
options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, 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.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla,
options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, 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.FestivalLocations.internal_name: options.FestivalLocations.option_disabled,
options.Fishsanity.internal_name: options.Fishsanity.option_none, options.Fishsanity.internal_name: options.Fishsanity.option_none,
options.Friendsanity.internal_name: options.Friendsanity.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.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla,
options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, 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.FestivalLocations.internal_name: options.FestivalLocations.option_disabled,
options.Fishsanity.internal_name: options.Fishsanity.option_none, options.Fishsanity.internal_name: options.Fishsanity.option_none,
options.Friendsanity.internal_name: options.Friendsanity.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 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 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("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") boots = self.create_item("JotPK: Progressive Boots")
gun = self.create_item("JotPK: Progressive Gun") 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.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 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("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)
self.remove(gun) 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.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 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("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)
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 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 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("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)
self.remove(gun) self.remove(gun)
self.remove(ammo) 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 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 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("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)
self.remove(gun) self.remove(gun)
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 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 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("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(boots) self.remove(boots)
self.remove(gun) self.remove(gun)

View File

@@ -7,18 +7,15 @@ class TestBooksLogic(SVTestBase):
options.Booksanity.internal_name: options.Booksanity.option_all, options.Booksanity.internal_name: options.Booksanity.option_all,
} }
def test_need_weapon_for_mapping_cave_systems(self): def test_can_get_mapping_cave_systems_with_weapon_and_time(self):
self.collect_lots_of_money(0.5) self.collect_months(12)
self.assert_cannot_reach_location("Read Mapping Cave Systems")
location = self.multiworld.get_location("Read Mapping Cave Systems", self.player)
self.assert_cannot_reach_location(location, self.multiworld.state)
self.collect("Progressive Mine Elevator") self.collect("Progressive Mine Elevator")
self.collect("Progressive Mine Elevator") self.collect("Progressive Mine Elevator")
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.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): 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.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): def test_big_coop_blueprint(self):
big_coop_blueprint_rule = self.world.logic.region.can_reach_location("Big Coop Blueprint") self.assert_cannot_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.collect_lots_of_money() self.collect_lots_of_money()
self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), self.assert_cannot_reach_location("Big Coop Blueprint")
f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}")
self.multiworld.state.collect(self.create_item("Progressive Coop")) self.multiworld.state.collect(self.create_item("Progressive Coop"))
self.assertTrue(big_coop_blueprint_rule(self.multiworld.state), self.assert_can_reach_location("Big Coop Blueprint")
f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}")
def test_deluxe_coop_blueprint(self): 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.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.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.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): def test_big_shed_blueprint(self):
big_shed_rule = self.world.logic.region.can_reach_location("Big Shed Blueprint") self.assert_cannot_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.collect_lots_of_money() self.collect_lots_of_money()
self.assertFalse(big_shed_rule(self.multiworld.state), self.assert_cannot_reach_location("Big Shed Blueprint")
f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}")
self.multiworld.state.collect(self.create_item("Progressive Shed")) self.multiworld.state.collect(self.create_item("Progressive Shed"))
self.assertTrue(big_shed_rule(self.multiworld.state), self.assert_can_reach_location("Big Shed Blueprint")
f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}")

View File

@@ -11,10 +11,10 @@ class TestBundlesLogic(SVTestBase):
} }
def test_vault_2500g_bundle(self): 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.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): class TestRemixedBundlesLogic(SVTestBase):
@@ -25,10 +25,10 @@ class TestRemixedBundlesLogic(SVTestBase):
} }
def test_sticky_bundle_has_grind_rules(self): 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.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): 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 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): 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 Raccoon", 6)
self.collect("Progressive Mine Elevator", 24) self.collect("Progressive Mine Elevator", 24)
self.collect("Mining Level", 12) self.collect("Mining Level", 12)
@@ -58,10 +53,12 @@ class TestRaccoonBundlesLogic(SVTestBase):
self.collect("Fishing Level", 10) self.collect("Fishing Level", 10)
self.collect("Furnace Recipe") self.collect("Furnace Recipe")
self.assertFalse(raccoon_rule_1(self.multiworld.state)) # The first raccoon bundle is a fishing one
self.assertFalse(raccoon_rule_3(self.multiworld.state)) 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.collect("Fish Smoker Recipe")
self.assertTrue(raccoon_rule_1(self.multiworld.state)) self.assert_can_reach_location("Raccoon Request 1")
self.assertTrue(raccoon_rule_3(self.multiworld.state)) self.assert_can_reach_location("Raccoon Request 3")

View File

@@ -14,18 +14,17 @@ class TestRecipeLearnLogic(SVTestBase):
def test_can_learn_qos_recipe(self): def test_can_learn_qos_recipe(self):
location = "Cook Radish Salad" location = "Cook Radish Salad"
rule = self.world.logic.region.can_reach_location(location) self.assert_cannot_reach_location(location)
self.assert_rule_false(rule, self.multiworld.state)
self.multiworld.state.collect(self.create_item("Progressive House")) 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("Radish Seeds"))
self.multiworld.state.collect(self.create_item("Spring")) self.multiworld.state.collect(self.create_item("Spring"))
self.multiworld.state.collect(self.create_item("Summer")) self.multiworld.state.collect(self.create_item("Summer"))
self.collect_lots_of_money() 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.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): class TestRecipeReceiveLogic(SVTestBase):
@@ -39,34 +38,32 @@ class TestRecipeReceiveLogic(SVTestBase):
def test_can_learn_qos_recipe(self): def test_can_learn_qos_recipe(self):
location = "Cook Radish Salad" location = "Cook Radish Salad"
rule = self.world.logic.region.can_reach_location(location) self.assert_cannot_reach_location(location)
self.assert_rule_false(rule, self.multiworld.state)
self.multiworld.state.collect(self.create_item("Progressive House")) 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("Radish Seeds"))
self.multiworld.state.collect(self.create_item("Summer")) self.multiworld.state.collect(self.create_item("Summer"))
self.collect_lots_of_money() self.collect_lots_of_money()
self.assert_rule_false(rule, self.multiworld.state) self.assert_cannot_reach_location(location)
spring = self.create_item("Spring") spring = self.create_item("Spring")
qos = self.create_item("The Queen of Sauce") qos = self.create_item("The Queen of Sauce")
self.multiworld.state.collect(spring) self.multiworld.state.collect(spring)
self.multiworld.state.collect(qos) 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(spring)
self.multiworld.state.remove(qos) self.multiworld.state.remove(qos)
self.multiworld.state.collect(self.create_item("Radish Salad Recipe")) 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): def test_get_chefsanity_check_recipe(self):
location = "Radish Salad Recipe" location = "Radish Salad Recipe"
rule = self.world.logic.region.can_reach_location(location) self.assert_cannot_reach_location(location)
self.assert_rule_false(rule, self.multiworld.state)
self.multiworld.state.collect(self.create_item("Spring")) self.multiworld.state.collect(self.create_item("Spring"))
self.collect_lots_of_money() 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") seeds = self.create_item("Radish Seeds")
summer = self.create_item("Summer") summer = self.create_item("Summer")
@@ -74,10 +71,10 @@ class TestRecipeReceiveLogic(SVTestBase):
self.multiworld.state.collect(seeds) self.multiworld.state.collect(seeds)
self.multiworld.state.collect(summer) self.multiworld.state.collect(summer)
self.multiworld.state.collect(house) 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(seeds)
self.multiworld.state.remove(summer) self.multiworld.state.remove(summer)
self.multiworld.state.remove(house) self.multiworld.state.remove(house)
self.multiworld.state.collect(self.create_item("The Queen of Sauce")) 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): 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 Pickaxe")] * 4)
self.collect([self.create_item("Progressive Fishing Rod")] * 4) self.collect([self.create_item("Progressive Fishing Rod")] * 4)
self.collect([self.create_item("Progressive Sword")] * 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("Combat Level")] * 10)
self.collect([self.create_item("Fishing Level")] * 10) self.collect([self.create_item("Fishing Level")] * 10)
self.collect_all_the_money() 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.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): def test_can_learn_crafting_recipe(self):
location = "Marble Brazier Recipe" self.assert_cannot_reach_location("Marble Brazier Recipe")
rule = self.world.logic.region.can_reach_location(location)
self.assert_rule_false(rule, self.multiworld.state)
self.collect_lots_of_money() 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): def test_can_craft_festival_recipe(self):
recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] 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.multiworld.state.collect(self.create_item("Torch Recipe"))
self.collect_lots_of_money() self.collect_lots_of_money()
rule = self.world.logic.crafting.can_craft(recipe) 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.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.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): def test_require_furnace_recipe_for_smelting_checks(self):
locations = ["Craft Furnace", "Smelting", "Copper Pickaxe Upgrade", "Gold Trash Can Upgrade"] 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.multiworld.state.collect(self.create_item("Fall"))
self.collect_lots_of_money() self.collect_lots_of_money()
rule = self.world.logic.crafting.can_craft(recipe) 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.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.multiworld.state.collect(self.create_item("Torch Recipe"))
self.assert_rule_true(rule, self.multiworld.state) self.assert_rule_true(rule)
class TestNoCraftsanityLogic(SVTestBase): class TestNoCraftsanityLogic(SVTestBase):
@@ -105,7 +101,7 @@ class TestNoCraftsanityLogic(SVTestBase):
def test_can_craft_recipe(self): def test_can_craft_recipe(self):
recipe = all_crafting_recipes_by_name["Wood Floor"] recipe = all_crafting_recipes_by_name["Wood Floor"]
rule = self.world.logic.crafting.can_craft(recipe) 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): def test_can_craft_festival_recipe(self):
recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] recipe = all_crafting_recipes_by_name["Jack-O-Lantern"]
@@ -116,7 +112,7 @@ class TestNoCraftsanityLogic(SVTestBase):
self.assertFalse(result) self.assertFalse(result)
self.collect([self.create_item("Progressive Season")] * 2) 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): def test_requires_mining_levels_for_smelting_checks(self):
locations = ["Smelting", "Copper Pickaxe Upgrade", "Gold Trash Can Upgrade"] 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.multiworld.state.collect(self.create_item("Fall"))
self.collect_lots_of_money() self.collect_lots_of_money()
rule = self.world.logic.crafting.can_craft(recipe) 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.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) self.collect_all_except(railroad_item)
for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: 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)) self.multiworld.state.collect(self.create_item(railroad_item))
for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: 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): class TestDonationLogicRandomized(SVTestBase):
@@ -37,12 +37,12 @@ class TestDonationLogicRandomized(SVTestBase):
LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags] LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags]
for donation in donation_locations: 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)) self.multiworld.state.collect(self.create_item(railroad_item))
for donation in donation_locations: 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): class TestDonationLogicMilestones(SVTestBase):
@@ -56,12 +56,12 @@ class TestDonationLogicMilestones(SVTestBase):
self.collect_all_except(railroad_item) self.collect_all_except(railroad_item)
for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: 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)) self.multiworld.state.collect(self.create_item(railroad_item))
for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: 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): def swap_museum_and_bathhouse(multiworld, player):

View File

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

View File

@@ -47,12 +47,8 @@ class TestFriendsanityDatingRules(SVTestBase):
for i in range(1, max_reachable + 1): for i in range(1, max_reachable + 1):
if i % step != 0 and i != 14: if i % step != 0 and i != 14:
continue continue
location = f"{prefix}{npc} {i}{suffix}" self.assert_can_reach_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")
for i in range(max_reachable + 1, 14 + 1): for i in range(max_reachable + 1, 14 + 1):
if i % step != 0 and i != 14: if i % step != 0 and i != 14:
continue continue
location = f"{prefix}{npc} {i}{suffix}" self.assert_cannot_reach_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")

View File

@@ -76,10 +76,8 @@ class TestShipsanityEverything(SVTestBase):
for location in shipsanity_locations: for location in shipsanity_locations:
with self.subTest(location.name): 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) self.collect(bin_item)
shipsanity_rule = self.world.logic.region.can_reach_location(location.name) self.assert_can_reach_location(location.name)
self.assert_rule_true(shipsanity_rule, self.multiworld.state)
self.remove(bin_item) self.remove(bin_item)

View File

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

View File

@@ -18,37 +18,37 @@ class TestProgressiveToolsLogic(SVTestBase):
self.multiworld.state.prog_items = {1: Counter()} self.multiworld.state.prog_items = {1: Counter()}
sturgeon_rule = self.world.logic.has("Sturgeon") 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") summer = self.create_item("Summer")
self.multiworld.state.collect(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") fishing_rod = self.create_item("Progressive Fishing Rod")
self.multiworld.state.collect(fishing_rod) self.multiworld.state.collect(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") fishing_level = self.create_item("Fishing Level")
self.multiworld.state.collect(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.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.remove(summer)
self.assert_rule_false(sturgeon_rule, self.multiworld.state) self.assert_rule_false(sturgeon_rule)
winter = self.create_item("Winter") winter = self.create_item("Winter")
self.multiworld.state.collect(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.remove(fishing_rod)
self.assert_rule_false(sturgeon_rule, self.multiworld.state) self.assert_rule_false(sturgeon_rule)
def test_old_master_cannoli(self): def test_old_master_cannoli(self):
self.multiworld.state.prog_items = {1: Counter()} self.multiworld.state.prog_items = {1: Counter()}
@@ -58,35 +58,34 @@ class TestProgressiveToolsLogic(SVTestBase):
self.multiworld.state.collect(self.create_item("Summer")) self.multiworld.state.collect(self.create_item("Summer"))
self.collect_lots_of_money() self.collect_lots_of_money()
rule = self.world.logic.region.can_reach_location("Old Master Cannoli") self.assert_cannot_reach_location("Old Master Cannoli")
self.assert_rule_false(rule, self.multiworld.state)
fall = self.create_item("Fall") fall = self.create_item("Fall")
self.multiworld.state.collect(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") tuesday = self.create_item("Traveling Merchant: Tuesday")
self.multiworld.state.collect(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") rare_seed = self.create_item("Rare Seed")
self.multiworld.state.collect(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.remove(fall)
self.assert_rule_false(rule, self.multiworld.state) self.assert_cannot_reach_location("Old Master Cannoli")
self.remove(tuesday) self.remove(tuesday)
green_house = self.create_item("Greenhouse") green_house = self.create_item("Greenhouse")
self.multiworld.state.collect(green_house) 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") friday = self.create_item("Traveling Merchant: Friday")
self.multiworld.state.collect(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.remove(green_house)
self.assert_rule_false(rule, self.multiworld.state) self.assert_cannot_reach_location("Old Master Cannoli")
self.remove(friday) 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 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]: 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)) 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 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]: 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): def test_cannot_get_fishing_rod_without_willy_access(self):
railroad_item = "Railroad Boulder Removed" railroad_item = "Railroad Boulder Removed"
@@ -120,12 +119,12 @@ class TestToolVanillaRequiresBlacksmith(SVTestBase):
self.collect_all_except(railroad_item) self.collect_all_except(railroad_item)
for fishing_rod_level in [3, 4]: 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)) self.multiworld.state.collect(self.create_item(railroad_item))
for fishing_rod_level in [3, 4]: 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): def place_region_at_entrance(multiworld, player, region, entrance):