Core: move option results to the World class instead of MultiWorld (#993)

🤞 

* map option objects to a `World.options` dict

* convert RoR2 to options dict system for testing

* add temp behavior for lttp with notes

* copy/paste bad

* convert `set_default_common_options` to a namespace property

* reorganize test call order

* have fill_restrictive use the new options system

* update world api

* update soe tests

* fix world api

* core: auto initialize a dataclass on the World class with the option results

* core: auto initialize a dataclass on the World class with the option results: small tying improvement

* add `as_dict` method to the options dataclass

* fix namespace issues with tests

* have current option updates use `.value` instead of changing the option

* update ror2 to use the new options system again

* revert the junk pool dict since it's cased differently

* fix begin_with_loop typo

* write new and old options to spoiler

* change factorio option behavior back

* fix comparisons

* move common and per_game_common options to new system

* core: automatically create missing options_dataclass from legacy option_definitions

* remove spoiler special casing and add back the Factorio option changing but in new system

* give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly

* reimplement `inspect.get_annotations`

* move option info generation for webhost to new system

* need to include Common and PerGame common since __annotations__ doesn't include super

* use get_type_hints for the options dictionary

* typing.get_type_hints returns the bases too.

* forgot to sweep through generate

* sweep through all the tests

* swap to a metaclass property

* move remaining usages from get_type_hints to metaclass property

* move remaining usages from __annotations__ to metaclass property

* move remaining usages from legacy dictionaries to metaclass property

* remove legacy dictionaries

* cache the metaclass property

* clarify inheritance in world api

* move the messenger to new options system

* add an assert for my dumb

* update the doc

* rename o to options

* missed a spot

* update new messenger options

* comment spacing

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* fix tests

* fix missing import

* make the documentation definition more accurate

* use options system for loc creation

* type cast MessengerWorld

* fix typo and use quotes for cast

* LTTP: set random seed in tests

* ArchipIdle: remove change here as it's default on AutoWorld

* Stardew: Need to set state because `set_default_common_options` used to

* The Messenger: update shop rando and helpers to new system; optimize imports

* Add a kwarg to `as_dict` to do the casing for you

* RoR2: use new kwarg for less code

* RoR2: revert some accidental reverts

* The Messenger: remove an unnecessary variable

* remove TypeVar that isn't used

* CommonOptions not abstract

* Docs: fix mistake in options api.md

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* create options for item link worlds

* revert accidental doc removals

* Item Links: set default options on group

* change Zillion to new options dataclass

* remove unused parameter to function

* use TypeGuard for Literal narrowing

* move dlc quest to new api

* move overcooked 2 to new api

* fixed some missed code in oc2

* - Tried to be compliant with 993 (WIP?)

* - I think it all works now

* - Removed last trace of me touching core

* typo

* It now passes all tests!

* Improve options, fix all issues I hope

* - Fixed init options

* dlcquest: fix bad imports

* missed a file

* - Reduce code duplication

* add as_dict documentation

* - Use .items(), get option name more directly, fix slot data content

* - Remove generic options from the slot data

* improve slot data documentation

* remove `CommonOptions.get_value` (#21)

* better slot data description

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@yahoo.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
This commit is contained in:
Aaron Wagener 2023-10-10 15:30:20 -05:00 committed by GitHub
parent a7b4914bb7
commit 7193182294
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 1587 additions and 1603 deletions

View File

@ -226,25 +226,24 @@ class MultiWorld():
range(1, self.players + 1)}
def set_options(self, args: Namespace) -> None:
for option_key in Options.common_options:
setattr(self, option_key, getattr(args, option_key, {}))
for option_key in Options.per_game_common_options:
setattr(self, option_key, getattr(args, option_key, {}))
for player in self.player_ids:
self.custom_data[player] = {}
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
for option_key in world_type.option_definitions:
setattr(self, option_key, getattr(args, option_key, {}))
self.worlds[player] = world_type(self, player)
self.worlds[player].random = self.per_slot_randoms[player]
for option_key in world_type.options_dataclass.type_hints:
option_values = getattr(args, option_key, {})
setattr(self, option_key, option_values)
# TODO - remove this loop once all worlds use options dataclasses
options_dataclass: typing.Type[Options.PerGameCommonOptions] = self.worlds[player].options_dataclass
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
for option_key in options_dataclass.type_hints})
def set_item_links(self):
item_links = {}
replacement_prio = [False, True, None]
for player in self.player_ids:
for item_link in self.item_links[player].value:
for item_link in self.worlds[player].options.item_links.value:
if item_link["name"] in item_links:
if item_links[item_link["name"]]["game"] != self.game[player]:
raise Exception(f"Cannot ItemLink across games. Link: {item_link['name']}")
@ -299,14 +298,6 @@ class MultiWorld():
group["non_local_items"] = item_link["non_local_items"]
group["link_replacement"] = replacement_prio[item_link["link_replacement"]]
# intended for unittests
def set_default_common_options(self):
for option_key, option in Options.common_options.items():
setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids})
for option_key, option in Options.per_game_common_options.items():
setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids})
self.state = CollectionState(self)
def secure(self):
self.random = ThreadBarrierProxy(secrets.SystemRandom())
self.is_race = True
@ -863,19 +854,19 @@ class Region:
"""
Adds locations to the Region object, where location_type is your Location class and locations is a dict of
location names to address.
:param locations: dictionary of locations to be created and added to this Region `{name: ID}`
:param location_type: Location class to be used to create the locations with"""
if location_type is None:
location_type = Location
for location, address in locations.items():
self.locations.append(location_type(self.player, location, address, self))
def connect(self, connecting_region: Region, name: Optional[str] = None,
rule: Optional[Callable[[CollectionState], bool]] = None) -> None:
"""
Connects this Region to another Region, placing the provided rule on the connection.
:param connecting_region: Region object to connect to path is `self -> exiting_region`
:param name: name of the connection being created
:param rule: callable to determine access of this connection to go from self to the exiting_region"""
@ -883,11 +874,11 @@ class Region:
if rule:
exit_.access_rule = rule
exit_.connect(connecting_region)
def create_exit(self, name: str) -> Entrance:
"""
Creates and returns an Entrance object as an exit of this region.
:param name: name of the Entrance being created
"""
exit_ = self.entrance_type(self.player, name, self)
@ -1257,7 +1248,7 @@ class Spoiler:
def to_file(self, filename: str) -> None:
def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
res = getattr(self.multiworld, option_key)[player]
res = getattr(self.multiworld.worlds[player].options, option_key)
display_name = getattr(option_obj, "display_name", option_key)
outfile.write(f"{display_name + ':':33}{res.current_option_name}\n")
@ -1275,8 +1266,7 @@ class Spoiler:
outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player)))
outfile.write('Game: %s\n' % self.multiworld.game[player])
options = ChainMap(Options.per_game_common_options, self.multiworld.worlds[player].option_definitions)
for f_option, option in options.items():
for f_option, option in self.multiworld.worlds[player].options_dataclass.type_hints.items():
write_option(f_option, option)
AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile)

12
Fill.py
View File

@ -5,6 +5,8 @@ import typing
from collections import Counter, deque
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
from Options import Accessibility
from worlds.AutoWorld import call_all
from worlds.generic.Rules import add_item_rule
@ -70,7 +72,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
spot_to_fill: typing.Optional[Location] = None
# if minimal accessibility, only check whether location is reachable if game not beatable
if world.accessibility[item_to_place.player] == 'minimal':
if world.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
perform_access_check = not world.has_beaten_game(maximum_exploration_state,
item_to_place.player) \
if single_player_placement else not has_beaten_game
@ -265,7 +267,7 @@ def fast_fill(world: MultiWorld,
def accessibility_corrections(world: MultiWorld, state: CollectionState, locations, pool=[]):
maximum_exploration_state = sweep_from_pool(state, pool)
minimal_players = {player for player in world.player_ids if world.accessibility[player] == "minimal"}
minimal_players = {player for player in world.player_ids if world.worlds[player].options.accessibility == "minimal"}
unreachable_locations = [location for location in world.get_locations() if location.player in minimal_players and
not location.can_reach(maximum_exploration_state)]
for location in unreachable_locations:
@ -288,7 +290,7 @@ def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locat
unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)]
if unreachable_locations:
def forbid_important_item_rule(item: Item):
return not ((item.classification & 0b0011) and world.accessibility[item.player] != 'minimal')
return not ((item.classification & 0b0011) and world.worlds[item.player].options.accessibility != 'minimal')
for location in unreachable_locations:
add_item_rule(location, forbid_important_item_rule)
@ -531,9 +533,9 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
# If other players are below the threshold value, swap progression in this sphere into earlier spheres,
# which gives more locations available by this sphere.
balanceable_players: typing.Dict[int, float] = {
player: world.progression_balancing[player] / 100
player: world.worlds[player].options.progression_balancing / 100
for player in world.player_ids
if world.progression_balancing[player] > 0
if world.worlds[player].options.progression_balancing > 0
}
if not balanceable_players:
logging.info('Skipping multiworld progression balancing.')

View File

@ -157,7 +157,8 @@ def main(args=None, callback=ERmain):
for yaml in weights_cache[path]:
if category_name is None:
for category in yaml:
if category in AutoWorldRegister.world_types and key in Options.common_options:
if category in AutoWorldRegister.world_types and \
key in Options.CommonOptions.type_hints:
yaml[category][key] = option
elif category_name not in yaml:
logging.warning(f"Meta: Category {category_name} is not present in {path}.")
@ -340,7 +341,7 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
return get_choice(option_key, category_dict)
if game in AutoWorldRegister.world_types:
game_world = AutoWorldRegister.world_types[game]
options = ChainMap(game_world.option_definitions, Options.per_game_common_options)
options = game_world.options_dataclass.type_hints
if option_key in options:
if options[option_key].supports_weighting:
return get_choice(option_key, category_dict)
@ -445,8 +446,8 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
f"which is not enabled.")
ret = argparse.Namespace()
for option_key in Options.per_game_common_options:
if option_key in weights and option_key not in Options.common_options:
for option_key in Options.PerGameCommonOptions.type_hints:
if option_key in weights and option_key not in Options.CommonOptions.type_hints:
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
ret.game = get_choice("game", weights)
@ -466,16 +467,11 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
game_weights = weights[ret.game]
ret.name = get_choice('name', weights)
for option_key, option in Options.common_options.items():
for option_key, option in Options.CommonOptions.type_hints.items():
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
for option_key, option in world_type.option_definitions.items():
for option_key, option in world_type.options_dataclass.type_hints.items():
handle_option(ret, game_weights, option_key, option, plando_options)
for option_key, option in Options.per_game_common_options.items():
# skip setting this option if already set from common_options, defaulting to root option
if option_key not in world_type.option_definitions and \
(option_key not in Options.common_options or option_key in game_weights):
handle_option(ret, game_weights, option_key, option, plando_options)
if PlandoOptions.items in plando_options:
ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":

22
Main.py
View File

@ -108,7 +108,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.info('')
for player in world.player_ids:
for item_name, count in world.start_inventory[player].value.items():
for item_name, count in world.worlds[player].options.start_inventory.value.items():
for _ in range(count):
world.push_precollected(world.create_item(item_name, player))
@ -130,15 +130,15 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for player in world.player_ids:
# items can't be both local and non-local, prefer local
world.non_local_items[player].value -= world.local_items[player].value
world.non_local_items[player].value -= set(world.local_early_items[player])
world.worlds[player].options.non_local_items.value -= world.worlds[player].options.local_items.value
world.worlds[player].options.non_local_items.value -= set(world.local_early_items[player])
AutoWorld.call_all(world, "set_rules")
for player in world.player_ids:
exclusion_rules(world, player, world.exclude_locations[player].value)
world.priority_locations[player].value -= world.exclude_locations[player].value
for location_name in world.priority_locations[player].value:
exclusion_rules(world, player, world.worlds[player].options.exclude_locations.value)
world.worlds[player].options.priority_locations.value -= world.worlds[player].options.exclude_locations.value
for location_name in world.worlds[player].options.priority_locations.value:
try:
location = world.get_location(location_name, player)
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
@ -151,8 +151,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
if world.players > 1:
locality_rules(world)
else:
world.non_local_items[1].value = set()
world.local_items[1].value = set()
world.worlds[1].options.non_local_items.value = set()
world.worlds[1].options.local_items.value = set()
AutoWorld.call_all(world, "generate_basic")
@ -360,11 +360,11 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
f" {location}"
locations_data[location.player][location.address] = \
location.item.code, location.item.player, location.item.flags
if location.name in world.start_location_hints[location.player]:
if location.name in world.worlds[location.player].options.start_location_hints:
precollect_hint(location)
elif location.item.name in world.start_hints[location.item.player]:
elif location.item.name in world.worlds[location.item.player].options.start_hints:
precollect_hint(location)
elif any([location.item.name in world.start_hints[player]
elif any([location.item.name in world.worlds[player].options.start_hints
for player in world.groups.get(location.item.player, {}).get("players", [])]):
precollect_hint(location)

View File

@ -2,6 +2,9 @@ from __future__ import annotations
import abc
import logging
from copy import deepcopy
from dataclasses import dataclass
import functools
import math
import numbers
import random
@ -211,6 +214,12 @@ class NumericOption(Option[int], numbers.Integral, abc.ABC):
else:
return self.value > other
def __ge__(self, other: typing.Union[int, NumericOption]) -> bool:
if isinstance(other, NumericOption):
return self.value >= other.value
else:
return self.value >= other
def __bool__(self) -> bool:
return bool(self.value)
@ -896,10 +905,55 @@ class ProgressionBalancing(SpecialRange):
}
common_options = {
"progression_balancing": ProgressionBalancing,
"accessibility": Accessibility
}
class OptionsMetaProperty(type):
def __new__(mcs,
name: str,
bases: typing.Tuple[type, ...],
attrs: typing.Dict[str, typing.Any]) -> "OptionsMetaProperty":
for attr_type in attrs.values():
assert not isinstance(attr_type, AssembleOptions),\
f"Options for {name} should be type hinted on the class, not assigned"
return super().__new__(mcs, name, bases, attrs)
@property
@functools.lru_cache(maxsize=None)
def type_hints(cls) -> typing.Dict[str, typing.Type[Option[typing.Any]]]:
"""Returns type hints of the class as a dictionary."""
return typing.get_type_hints(cls)
@dataclass
class CommonOptions(metaclass=OptionsMetaProperty):
progression_balancing: ProgressionBalancing
accessibility: Accessibility
def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]:
"""
Returns a dictionary of [str, Option.value]
:param option_names: names of the options to return
:param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`
"""
option_results = {}
for option_name in option_names:
if option_name in type(self).type_hints:
if casing == "snake":
display_name = option_name
elif casing == "camel":
split_name = [name.title() for name in option_name.split("_")]
split_name[0] = split_name[0].lower()
display_name = "".join(split_name)
elif casing == "pascal":
display_name = "".join([name.title() for name in option_name.split("_")])
elif casing == "kebab":
display_name = option_name.replace("_", "-")
else:
raise ValueError(f"{casing} is invalid casing for as_dict. "
"Valid names are 'snake', 'camel', 'pascal', 'kebab'.")
option_results[display_name] = getattr(self, option_name).value
else:
raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}")
return option_results
class LocalItems(ItemSet):
@ -1020,17 +1074,16 @@ class ItemLinks(OptionList):
link.setdefault("link_replacement", None)
per_game_common_options = {
**common_options, # can be overwritten per-game
"local_items": LocalItems,
"non_local_items": NonLocalItems,
"start_inventory": StartInventory,
"start_hints": StartHints,
"start_location_hints": StartLocationHints,
"exclude_locations": ExcludeLocations,
"priority_locations": PriorityLocations,
"item_links": ItemLinks
}
@dataclass
class PerGameCommonOptions(CommonOptions):
local_items: LocalItems
non_local_items: NonLocalItems
start_inventory: StartInventory
start_hints: StartHints
start_location_hints: StartLocationHints
exclude_locations: ExcludeLocations
priority_locations: PriorityLocations
item_links: ItemLinks
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True):
@ -1071,10 +1124,7 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
for game_name, world in AutoWorldRegister.world_types.items():
if not world.hidden or generate_hidden:
all_options: typing.Dict[str, AssembleOptions] = {
**per_game_common_options,
**world.option_definitions
}
all_options: typing.Dict[str, AssembleOptions] = world.options_dataclass.type_hints
with open(local_path("data", "options.yaml")) as f:
file_data = f.read()

View File

@ -36,10 +36,7 @@ def create():
for game_name, world in AutoWorldRegister.world_types.items():
all_options: typing.Dict[str, Options.AssembleOptions] = {
**Options.per_game_common_options,
**world.option_definitions
}
all_options: typing.Dict[str, Options.AssembleOptions] = world.options_dataclass.type_hints
# Generate JSON files for player-settings pages
player_settings = {

View File

@ -28,19 +28,23 @@ Choice, and defining `alias_true = option_full`.
and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's
create our option class (with a docstring), give it a `display_name`, and add it to a dictionary that keeps track of our
options:
create our option class (with a docstring), give it a `display_name`, and add it to our game's options dataclass:
```python
# Options.py
from dataclasses import dataclass
from Options import Toggle, PerGameCommonOptions
class StartingSword(Toggle):
"""Adds a sword to your starting inventory."""
display_name = "Start With Sword"
example_options = {
"starting_sword": StartingSword
}
@dataclass
class ExampleGameOptions(PerGameCommonOptions):
starting_sword: StartingSword
```
This will create a `Toggle` option, internally called `starting_sword`. To then submit this to the multiworld, we add it
@ -48,27 +52,30 @@ to our world's `__init__.py`:
```python
from worlds.AutoWorld import World
from .Options import options
from .Options import ExampleGameOptions
class ExampleWorld(World):
option_definitions = options
# this gives the generator all the definitions for our options
options_dataclass = ExampleGameOptions
# this gives us typing hints for all the options we defined
options: ExampleGameOptions
```
### Option Checking
Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after
world instantiation. These are created as attributes on the MultiWorld and can be accessed with
`self.multiworld.my_option_name[self.player]`. This is the option class, which supports direct comparison methods to
`self.options.my_option_name`. This is an instance of the option class, which supports direct comparison methods to
relevant objects (like comparing a Toggle class to a `bool`). If you need to access the option result directly, this is
the option class's `value` attribute. For our example above we can do a simple check:
```python
if self.multiworld.starting_sword[self.player]:
if self.options.starting_sword:
do_some_things()
```
or if I need a boolean object, such as in my slot_data I can access it as:
```python
start_with_sword = bool(self.multiworld.starting_sword[self.player].value)
start_with_sword = bool(self.options.starting_sword.value)
```
## Generic Option Classes
@ -120,7 +127,7 @@ Like Toggle, but 1 (true) is the default value.
A numeric option allowing you to define different sub options. Values are stored as integers, but you can also do
comparison methods with the class and strings, so if you have an `option_early_sword`, this can be compared with:
```python
if self.multiworld.sword_availability[self.player] == "early_sword":
if self.options.sword_availability == "early_sword":
do_early_sword_things()
```
@ -128,7 +135,7 @@ or:
```python
from .Options import SwordAvailability
if self.multiworld.sword_availability[self.player] == SwordAvailability.option_early_sword:
if self.options.sword_availability == SwordAvailability.option_early_sword:
do_early_sword_things()
```
@ -160,7 +167,7 @@ within the world.
Like choice allows you to predetermine options and has all of the same comparison methods and handling. Also accepts any
user defined string as a valid option, so will either need to be validated by adding a validation step to the option
class or within world, if necessary. Value for this class is `Union[str, int]` so if you need the value at a specified
point, `self.multiworld.my_option[self.player].current_key` will always return a string.
point, `self.options.my_option.current_key` will always return a string.
### PlandoBosses
An option specifically built for handling boss rando, if your game can use it. Is a subclass of TextChoice so supports

View File

@ -86,9 +86,11 @@ inside a `World` object.
### Player Options
Players provide customized settings for their World in the form of yamls.
Those are accessible through `self.multiworld.<option_name>[self.player]`. A dict
of valid options has to be provided in `self.option_definitions`. Options are automatically
added to the `World` object for easy access.
A `dataclass` of valid options definitions has to be provided in `self.options_dataclass`.
(It must be a subclass of `PerGameCommonOptions`.)
Option results are automatically added to the `World` object for easy access.
Those are accessible through `self.options.<option_name>`, and you can get a dictionary of the option values via
`self.options.as_dict(<option_names>)`, passing the desired options as strings.
### World Settings
@ -221,11 +223,11 @@ See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requireme
AP will only import the `__init__.py`. Depending on code size it makes sense to
use multiple files and use relative imports to access them.
e.g. `from .Options import mygame_options` from your `__init__.py` will load
`worlds/<world_name>/Options.py` and make its `mygame_options` accessible.
e.g. `from .Options import MyGameOptions` from your `__init__.py` will load
`world/[world_name]/Options.py` and make its `MyGameOptions` accessible.
When imported names pile up it may be easier to use `from . import Options`
and access the variable as `Options.mygame_options`.
and access the variable as `Options.MyGameOptions`.
Imports from directories outside your world should use absolute imports.
Correct use of relative / absolute imports is required for zipped worlds to
@ -273,8 +275,9 @@ Each option has its own class, inherits from a base option type, has a docstring
to describe it and a `display_name` property for display on the website and in
spoiler logs.
The actual name as used in the yaml is defined in a `Dict[str, AssembleOptions]`, that is
assigned to the world under `self.option_definitions`.
The actual name as used in the yaml is defined via the field names of a `dataclass` that is
assigned to the world under `self.options_dataclass`. By convention, the strings
that define your option names should be in `snake_case`.
Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`.
For more see `Options.py` in AP's base directory.
@ -309,8 +312,8 @@ default = 0
```python
# Options.py
from Options import Toggle, Range, Choice, Option
import typing
from dataclasses import dataclass
from Options import Toggle, Range, Choice, PerGameCommonOptions
class Difficulty(Choice):
"""Sets overall game difficulty."""
@ -333,23 +336,27 @@ class FixXYZGlitch(Toggle):
"""Fixes ABC when you do XYZ"""
display_name = "Fix XYZ Glitch"
# By convention we call the options dict variable `<world>_options`.
mygame_options: typing.Dict[str, AssembleOptions] = {
"difficulty": Difficulty,
"final_boss_hp": FinalBossHP,
"fix_xyz_glitch": FixXYZGlitch,
}
# By convention, we call the options dataclass `<world>Options`.
# It has to be derived from 'PerGameCommonOptions'.
@dataclass
class MyGameOptions(PerGameCommonOptions):
difficulty: Difficulty
final_boss_hp: FinalBossHP
fix_xyz_glitch: FixXYZGlitch
```
```python
# __init__.py
from worlds.AutoWorld import World
from .Options import mygame_options # import the options dict
from .Options import MyGameOptions # import the options dataclass
class MyGameWorld(World):
#...
option_definitions = mygame_options # assign the options dict to the world
#...
# ...
options_dataclass = MyGameOptions # assign the options dataclass to the world
options: MyGameOptions # typing for option results
# ...
```
### A World Class Skeleton
@ -359,13 +366,14 @@ class MyGameWorld(World):
import settings
import typing
from .Options import mygame_options # the options we defined earlier
from .Options import MyGameOptions # the options we defined earlier
from .Items import mygame_items # data used below to add items to the World
from .Locations import mygame_locations # same as above
from worlds.AutoWorld import World
from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification
class MyGameItem(Item): # or from Items import MyGameItem
game = "My Game" # name of the game/world this item is from
@ -374,6 +382,7 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation
game = "My Game" # name of the game/world this location is in
class MyGameSettings(settings.Group):
class RomFile(settings.SNESRomPath):
"""Insert help text for host.yaml here."""
@ -384,7 +393,8 @@ class MyGameSettings(settings.Group):
class MyGameWorld(World):
"""Insert description of the world/game here."""
game = "My Game" # name of the game/world
option_definitions = mygame_options # options the player can set
options_dataclass = MyGameOptions # options the player can set
options: MyGameOptions # typing hints for option results
settings: typing.ClassVar[MyGameSettings] # will be automatically assigned from type hint
topology_present = True # show path to required location checks in spoiler
@ -460,7 +470,7 @@ In addition, the following methods can be implemented and are called in this ord
```python
def generate_early(self) -> None:
# read player settings to world instance
self.final_boss_hp = self.multiworld.final_boss_hp[self.player].value
self.final_boss_hp = self.options.final_boss_hp.value
```
#### create_item
@ -687,9 +697,9 @@ def generate_output(self, output_directory: str):
in self.multiworld.precollected_items[self.player]],
"final_boss_hp": self.final_boss_hp,
# store option name "easy", "normal" or "hard" for difficuly
"difficulty": self.multiworld.difficulty[self.player].current_key,
"difficulty": self.options.difficulty.current_key,
# store option value True or False for fixing a glitch
"fix_xyz_glitch": self.multiworld.fix_xyz_glitch[self.player].value,
"fix_xyz_glitch": self.options.fix_xyz_glitch.value,
}
# point to a ROM specified by the installation
src = self.settings.rom_file
@ -702,6 +712,26 @@ def generate_output(self, output_directory: str):
generate_mod(src, out_file, data)
```
### Slot Data
If the game client needs to know information about the generated seed, a preferred method of transferring the data
is through the slot data. This can be filled from the `fill_slot_data` method of your world by returning a `Dict[str, Any]`,
but should be limited to data that is absolutely necessary to not waste resources. Slot data is sent to your client once
it has successfully [connected](network%20protocol.md#connected).
If you need to know information about locations in your world, instead
of propagating the slot data, it is preferable to use [LocationScouts](network%20protocol.md#locationscouts) since that
data already exists on the server. The most common usage of slot data is to send option results that the client needs
to be aware of.
```python
def fill_slot_data(self):
# in order for our game client to handle the generated seed correctly we need to know what the user selected
# for their difficulty and final boss HP
# a dictionary returned from this method gets set as the slot_data and will be sent to the client after connecting
# the options dataclass has a method to return a `Dict[str, Any]` of each option name provided and the option's value
return self.options.as_dict("difficulty", "final_boss_hp")
```
### Documentation
Each world implementation should have a tutorial and a game info page. These are both rendered on the website by reading

View File

@ -125,13 +125,13 @@ class WorldTestBase(unittest.TestCase):
self.multiworld.game[1] = self.game
self.multiworld.player_name = {1: "Tester"}
self.multiworld.set_seed(seed)
self.multiworld.state = CollectionState(self.multiworld)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].option_definitions.items():
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items():
setattr(args, name, {
1: option.from_any(self.options.get(name, getattr(option, "default")))
})
self.multiworld.set_options(args)
self.multiworld.set_default_common_options()
for step in gen_steps:
call_all(self.multiworld, step)

View File

@ -1,16 +1,20 @@
from typing import List, Iterable
import unittest
import Options
from Options import Accessibility
from worlds.AutoWorld import World
from Fill import FillError, balance_multiworld_progression, fill_restrictive, \
distribute_early_items, distribute_items_restrictive
from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, Item, Location, \
ItemClassification
ItemClassification, CollectionState
from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule
def generate_multi_world(players: int = 1) -> MultiWorld:
multi_world = MultiWorld(players)
multi_world.player_name = {}
multi_world.state = CollectionState(multi_world)
for i in range(players):
player_id = i+1
world = World(multi_world, player_id)
@ -19,9 +23,16 @@ def generate_multi_world(players: int = 1) -> MultiWorld:
multi_world.player_name[player_id] = "Test Player " + str(player_id)
region = Region("Menu", player_id, multi_world, "Menu Region Hint")
multi_world.regions.append(region)
for option_key, option in Options.PerGameCommonOptions.type_hints.items():
if hasattr(multi_world, option_key):
getattr(multi_world, option_key).setdefault(player_id, option.from_any(getattr(option, "default")))
else:
setattr(multi_world, option_key, {player_id: option.from_any(getattr(option, "default"))})
# TODO - remove this loop once all worlds use options dataclasses
world.options = world.options_dataclass(**{option_key: getattr(multi_world, option_key)[player_id]
for option_key in world.options_dataclass.type_hints})
multi_world.set_seed(0)
multi_world.set_default_common_options()
return multi_world
@ -186,7 +197,7 @@ class TestFillRestrictive(unittest.TestCase):
items = player1.prog_items
locations = player1.locations
multi_world.accessibility[player1.id].value = multi_world.accessibility[player1.id].option_minimal
multi_world.worlds[player1.id].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
multi_world.completion_condition[player1.id] = lambda state: state.has(
items[1].name, player1.id)
set_rule(locations[1], lambda state: state.has(

View File

@ -1,3 +1,4 @@
from argparse import Namespace
from typing import Dict, Optional, Callable
from BaseClasses import MultiWorld, CollectionState, Region
@ -13,7 +14,6 @@ class TestHelpers(unittest.TestCase):
self.multiworld.game[self.player] = "helper_test_game"
self.multiworld.player_name = {1: "Tester"}
self.multiworld.set_seed()
self.multiworld.set_default_common_options()
def testRegionHelpers(self) -> None:
regions: Dict[str, str] = {

View File

@ -6,6 +6,6 @@ class TestOptions(unittest.TestCase):
def testOptionsHaveDocString(self):
for gamename, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden:
for option_key, option in world_type.option_definitions.items():
for option_key, option in world_type.options_dataclass.type_hints.items():
with self.subTest(game=gamename, option=option_key):
self.assertTrue(option.__doc__)

View File

@ -1,7 +1,7 @@
from argparse import Namespace
from typing import Type, Tuple
from BaseClasses import MultiWorld
from BaseClasses import MultiWorld, CollectionState
from worlds.AutoWorld import call_all, World
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
@ -12,11 +12,11 @@ def setup_solo_multiworld(world_type: Type[World], steps: Tuple[str, ...] = gen_
multiworld.game[1] = world_type.game
multiworld.player_name = {1: "Tester"}
multiworld.set_seed()
multiworld.state = CollectionState(multiworld)
args = Namespace()
for name, option in world_type.option_definitions.items():
for name, option in world_type.options_dataclass.type_hints.items():
setattr(args, name, {1: option.from_any(option.default)})
multiworld.set_options(args)
multiworld.set_default_common_options()
for step in steps:
call_all(multiworld, step)
return multiworld

View File

@ -4,11 +4,12 @@ import hashlib
import logging
import pathlib
import sys
from typing import Any, Callable, ClassVar, Dict, FrozenSet, List, Optional, Set, TYPE_CHECKING, TextIO, Tuple, Type, \
from dataclasses import make_dataclass
from typing import Any, Callable, ClassVar, Dict, Set, Tuple, FrozenSet, List, Optional, TYPE_CHECKING, TextIO, Type, \
Union
from Options import PerGameCommonOptions
from BaseClasses import CollectionState
from Options import AssembleOptions
if TYPE_CHECKING:
import random
@ -63,6 +64,12 @@ class AutoWorldRegister(type):
dct["required_client_version"] = max(dct["required_client_version"],
base.__dict__["required_client_version"])
# create missing options_dataclass from legacy option_definitions
# TODO - remove this once all worlds use options dataclasses
if "options_dataclass" not in dct and "option_definitions" in dct:
dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(),
bases=(PerGameCommonOptions,))
# construct class
new_class = super().__new__(mcs, name, bases, dct)
if "game" in dct:
@ -163,8 +170,11 @@ class World(metaclass=AutoWorldRegister):
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
A Game should have its own subclass of World in which it defines the required data structures."""
option_definitions: ClassVar[Dict[str, AssembleOptions]] = {}
options_dataclass: ClassVar[Type[PerGameCommonOptions]] = PerGameCommonOptions
"""link your Options mapping"""
options: PerGameCommonOptions
"""resulting options for the player of this world"""
game: ClassVar[str]
"""name the game"""
topology_present: ClassVar[bool] = False
@ -362,16 +372,14 @@ class World(metaclass=AutoWorldRegister):
def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World:
"""Creates a group, which is an instance of World that is responsible for multiple others.
An example case is ItemLinks creating these."""
import Options
# TODO remove loop when worlds use options dataclass
for option_key, option in cls.options_dataclass.type_hints.items():
getattr(multiworld, option_key)[new_player_id] = option(option.default)
group = cls(multiworld, new_player_id)
group.options = cls.options_dataclass(**{option_key: option(option.default)
for option_key, option in cls.options_dataclass.type_hints.items()})
for option_key, option in cls.option_definitions.items():
getattr(multiworld, option_key)[new_player_id] = option(option.default)
for option_key, option in Options.common_options.items():
getattr(multiworld, option_key)[new_player_id] = option(option.default)
for option_key, option in Options.per_game_common_options.items():
getattr(multiworld, option_key)[new_player_id] = option(option.default)
return cls(multiworld, new_player_id)
return group
# decent place to implement progressive items, in most cases can stay as-is
def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]:

View File

@ -0,0 +1,16 @@
import unittest
from argparse import Namespace
from BaseClasses import MultiWorld, CollectionState
from worlds import AutoWorldRegister
class LTTPTestBase(unittest.TestCase):
def world_setup(self):
self.multiworld = MultiWorld(1)
self.multiworld.state = CollectionState(self.multiworld)
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorldRegister.world_types["A Link to the Past"].options_dataclass.type_hints.items():
setattr(args, name, {1: option.from_any(getattr(option, "default"))})
self.multiworld.set_options(args)

View File

@ -1,25 +1,16 @@
import unittest
from argparse import Namespace
from BaseClasses import MultiWorld, CollectionState, ItemClassification
from worlds.alttp.Dungeons import get_dungeon_item_pool
from BaseClasses import CollectionState, ItemClassification
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import mandatory_connections, connect_simple
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops
from worlds import AutoWorld
from worlds.alttp.test import LTTPTestBase
class TestDungeon(unittest.TestCase):
class TestDungeon(LTTPTestBase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)})
self.multiworld.set_options(args)
self.multiworld.set_default_common_options()
self.world_setup()
self.starting_regions = [] # Where to start exploring
self.remove_exits = [] # Block dungeon exits
self.multiworld.difficulty_requirements[1] = difficulties['normal']

View File

@ -1,6 +1,3 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances
from worlds.alttp.InvertedRegions import create_inverted_regions
@ -10,17 +7,12 @@ from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops
from test.TestBase import TestBase
from worlds import AutoWorld
from worlds.alttp.test import LTTPTestBase
class TestInverted(TestBase):
class TestInverted(TestBase, LTTPTestBase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)})
self.multiworld.set_options(args)
self.multiworld.set_default_common_options()
self.world_setup()
self.multiworld.difficulty_requirements[1] = difficulties['normal']
self.multiworld.mode[1] = "inverted"
create_inverted_regions(self.multiworld, 1)

View File

@ -1,27 +1,17 @@
import unittest
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons
from worlds.alttp.EntranceShuffle import connect_entrance, Inverted_LW_Entrances, Inverted_LW_Dungeon_Entrances, Inverted_LW_Single_Cave_Doors, Inverted_Old_Man_Entrances, Inverted_DW_Entrances, Inverted_DW_Dungeon_Entrances, Inverted_DW_Single_Cave_Doors, \
Inverted_LW_Entrances_Must_Exit, Inverted_LW_Dungeon_Entrances_Must_Exit, Inverted_Bomb_Shop_Multi_Cave_Doors, Inverted_Bomb_Shop_Single_Cave_Doors, Blacksmith_Single_Cave_Doors, Inverted_Blacksmith_Multi_Cave_Doors
from worlds.alttp.InvertedRegions import create_inverted_regions
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Rules import set_inverted_big_bomb_rules
from worlds import AutoWorld
from worlds.alttp.test import LTTPTestBase
class TestInvertedBombRules(unittest.TestCase):
class TestInvertedBombRules(LTTPTestBase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
self.world_setup()
self.multiworld.mode[1] = "inverted"
args = Namespace
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)})
self.multiworld.set_options(args)
self.multiworld.set_default_common_options()
self.multiworld.difficulty_requirements[1] = difficulties['normal']
create_inverted_regions(self.multiworld, 1)
self.multiworld.worlds[1].create_dungeons()

View File

@ -1,27 +1,18 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances
from worlds.alttp.InvertedRegions import create_inverted_regions
from worlds.alttp.ItemPool import generate_itempool, difficulties
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase
from worlds import AutoWorld
from worlds.alttp.test import LTTPTestBase
class TestInvertedMinor(TestBase):
class TestInvertedMinor(TestBase, LTTPTestBase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)})
self.multiworld.set_options(args)
self.multiworld.set_default_common_options()
self.world_setup()
self.multiworld.mode[1] = "inverted"
self.multiworld.logic[1] = "minorglitches"
self.multiworld.difficulty_requirements[1] = difficulties['normal']

View File

@ -1,28 +1,18 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances
from worlds.alttp.InvertedRegions import create_inverted_regions
from worlds.alttp.ItemPool import generate_itempool, difficulties
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase
from worlds import AutoWorld
from worlds.alttp.test import LTTPTestBase
class TestInvertedOWG(TestBase):
class TestInvertedOWG(TestBase, LTTPTestBase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)})
self.multiworld.set_options(args)
self.multiworld.set_default_common_options()
self.world_setup()
self.multiworld.logic[1] = "owglitches"
self.multiworld.mode[1] = "inverted"
self.multiworld.difficulty_requirements[1] = difficulties['normal']

View File

@ -1,27 +1,15 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_entrances
from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.InvertedRegions import mark_dark_world_regions
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops
from test.TestBase import TestBase
from worlds import AutoWorld
from worlds.alttp.test import LTTPTestBase
class TestMinor(TestBase):
class TestMinor(TestBase, LTTPTestBase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)})
self.multiworld.set_options(args)
self.multiworld.set_default_common_options()
self.world_setup()
self.multiworld.logic[1] = "minorglitches"
self.multiworld.difficulty_requirements[1] = difficulties['normal']
self.multiworld.worlds[1].er_seed = 0

View File

@ -1,24 +1,15 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.InvertedRegions import mark_dark_world_regions
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory
from test.TestBase import TestBase
from worlds import AutoWorld
from worlds.alttp.test import LTTPTestBase
class TestVanillaOWG(TestBase):
class TestVanillaOWG(TestBase, LTTPTestBase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)})
self.multiworld.set_options(args)
self.multiworld.set_default_common_options()
self.world_setup()
self.multiworld.difficulty_requirements[1] = difficulties['normal']
self.multiworld.logic[1] = "owglitches"
self.multiworld.worlds[1].er_seed = 0

View File

@ -1,22 +1,14 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.InvertedRegions import mark_dark_world_regions
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory
from test.TestBase import TestBase
from worlds import AutoWorld
from worlds.alttp.test import LTTPTestBase
class TestVanilla(TestBase):
class TestVanilla(TestBase, LTTPTestBase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)})
self.multiworld.set_options(args)
self.multiworld.set_default_common_options()
self.world_setup()
self.multiworld.logic[1] = "noglitches"
self.multiworld.difficulty_requirements[1] = difficulties['normal']
self.multiworld.worlds[1].er_seed = 0

View File

@ -1,11 +1,12 @@
import csv
import enum
import math
from typing import Protocol, Union, Dict, List, Set
from BaseClasses import Item, ItemClassification
from . import Options, data
from dataclasses import dataclass, field
from random import Random
from typing import Dict, List, Set
from BaseClasses import Item, ItemClassification
from . import Options, data
class DLCQuestItem(Item):
@ -93,38 +94,35 @@ def create_trap_items(world, World_Options: Options.DLCQuestOptions, trap_needed
def create_items(world, World_Options: Options.DLCQuestOptions, locations_count: int, random: Random):
created_items = []
if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[
Options.Campaign] == Options.Campaign.option_both:
if World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign == Options.Campaign.option_both:
for item in items_by_group[Group.DLCQuest]:
if item.has_any_group(Group.DLC):
created_items.append(world.create_item(item))
if item.has_any_group(Group.Item) and World_Options[
Options.ItemShuffle] == Options.ItemShuffle.option_shuffled:
if item.has_any_group(Group.Item) and World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
created_items.append(world.create_item(item))
if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin:
coin_bundle_needed = math.floor(825 / World_Options[Options.CoinSanityRange])
if World_Options.coinsanity == Options.CoinSanity.option_coin:
coin_bundle_needed = math.floor(825 / World_Options.coinbundlequantity)
for item in items_by_group[Group.DLCQuest]:
if item.has_any_group(Group.Coin):
for i in range(coin_bundle_needed):
created_items.append(world.create_item(item))
if 825 % World_Options[Options.CoinSanityRange] != 0:
if 825 % World_Options.coinbundlequantity != 0:
created_items.append(world.create_item(item))
if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[
Options.Campaign] == Options.Campaign.option_both:
if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or
World_Options.campaign == Options.Campaign.option_both):
for item in items_by_group[Group.Freemium]:
if item.has_any_group(Group.DLC):
created_items.append(world.create_item(item))
if item.has_any_group(Group.Item) and World_Options[
Options.ItemShuffle] == Options.ItemShuffle.option_shuffled:
if item.has_any_group(Group.Item) and World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
created_items.append(world.create_item(item))
if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin:
coin_bundle_needed = math.floor(889 / World_Options[Options.CoinSanityRange])
if World_Options.coinsanity == Options.CoinSanity.option_coin:
coin_bundle_needed = math.floor(889 / World_Options.coinbundlequantity)
for item in items_by_group[Group.Freemium]:
if item.has_any_group(Group.Coin):
for i in range(coin_bundle_needed):
created_items.append(world.create_item(item))
if 889 % World_Options[Options.CoinSanityRange] != 0:
if 889 % World_Options.coinbundlequantity != 0:
created_items.append(world.create_item(item))
trap_items = create_trap_items(world, World_Options, locations_count - len(created_items), random)

View File

@ -1,5 +1,4 @@
from BaseClasses import Location, MultiWorld
from . import Options
from BaseClasses import Location
class DLCQuestLocation(Location):

View File

@ -1,22 +1,6 @@
from typing import Union, Dict, runtime_checkable, Protocol
from Options import Option, DeathLink, Choice, Toggle, SpecialRange
from dataclasses import dataclass
@runtime_checkable
class DLCQuestOption(Protocol):
internal_name: str
@dataclass
class DLCQuestOptions:
options: Dict[str, Union[bool, int]]
def __getitem__(self, item: Union[str, DLCQuestOption]) -> Union[bool, int]:
if isinstance(item, DLCQuestOption):
item = item.internal_name
return self.options.get(item, None)
from Options import Choice, DeathLink, PerGameCommonOptions, SpecialRange
class DoubleJumpGlitch(Choice):
@ -94,31 +78,13 @@ class ItemShuffle(Choice):
default = 0
DLCQuest_options: Dict[str, type(Option)] = {
option.internal_name: option
for option in [
DoubleJumpGlitch,
CoinSanity,
CoinSanityRange,
TimeIsMoney,
EndingChoice,
Campaign,
ItemShuffle,
]
}
default_options = {option.internal_name: option.default for option in DLCQuest_options.values()}
DLCQuest_options["death_link"] = DeathLink
def fetch_options(world, player: int) -> DLCQuestOptions:
return DLCQuestOptions({option: get_option_value(world, player, option) for option in DLCQuest_options})
def get_option_value(world, player: int, name: str) -> Union[bool, int]:
assert name in DLCQuest_options, f"{name} is not a valid option for DLC Quest."
value = getattr(world, name)
if issubclass(DLCQuest_options[name], Toggle):
return bool(value[player].value)
return value[player].value
@dataclass
class DLCQuestOptions(PerGameCommonOptions):
double_jump_glitch: DoubleJumpGlitch
coinsanity: CoinSanity
coinbundlequantity: CoinSanityRange
time_is_money: TimeIsMoney
ending_choice: EndingChoice
campaign: Campaign
item_shuffle: ItemShuffle
death_link: DeathLink

View File

@ -1,8 +1,9 @@
import math
from BaseClasses import MultiWorld, Region, Location, Entrance, ItemClassification
from BaseClasses import Entrance, MultiWorld, Region
from . import Options
from .Locations import DLCQuestLocation, location_table
from .Rules import create_event
from . import Options
DLCQuestRegion = ["Movement Pack", "Behind Tree", "Psychological Warfare", "Double Jump Left",
"Double Jump Behind the Tree", "The Forest", "Final Room"]
@ -26,16 +27,16 @@ def add_coin_dlcquest(region: Region, Coin: int, player: int):
def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQuestOptions):
Regmenu = Region("Menu", player, world)
if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[
Options.Campaign] == Options.Campaign.option_both:
if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign
== Options.Campaign.option_both):
Regmenu.exits += [Entrance(player, "DLC Quest Basic", Regmenu)]
if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[
Options.Campaign] == Options.Campaign.option_both:
if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign
== Options.Campaign.option_both):
Regmenu.exits += [Entrance(player, "Live Freemium or Die", Regmenu)]
world.regions.append(Regmenu)
if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[
Options.Campaign] == Options.Campaign.option_both:
if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign
== Options.Campaign.option_both):
Regmoveright = Region("Move Right", player, world, "Start of the basic game")
Locmoveright_name = ["Movement Pack", "Animation Pack", "Audio Pack", "Pause Menu Pack"]
@ -43,13 +44,13 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue
Regmoveright.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmoveright) for
loc_name in Locmoveright_name]
add_coin_dlcquest(Regmoveright, 4, player)
if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin:
coin_bundle_needed = math.floor(825 / World_Options[Options.CoinSanityRange])
if World_Options.coinsanity == Options.CoinSanity.option_coin:
coin_bundle_needed = math.floor(825 / World_Options.coinbundlequantity)
for i in range(coin_bundle_needed):
item_coin = f"DLC Quest: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin"
item_coin = f"DLC Quest: {World_Options.coinbundlequantity * (i + 1)} Coin"
Regmoveright.locations += [
DLCQuestLocation(player, item_coin, location_table[item_coin], Regmoveright)]
if 825 % World_Options[Options.CoinSanityRange] != 0:
if 825 % World_Options.coinbundlequantity != 0:
Regmoveright.locations += [
DLCQuestLocation(player, "DLC Quest: 825 Coin", location_table["DLC Quest: 825 Coin"],
Regmoveright)]
@ -58,7 +59,7 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue
Regmovpack = Region("Movement Pack", player, world)
Locmovpack_name = ["Time is Money Pack", "Psychological Warfare Pack", "Armor for your Horse Pack",
"Shepherd Sheep"]
if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled:
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
Locmovpack_name += ["Sword"]
Regmovpack.exits = [Entrance(player, "Tree", Regmovpack), Entrance(player, "Cloud", Regmovpack)]
Regmovpack.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmovpack) for loc_name
@ -68,7 +69,7 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue
Regbtree = Region("Behind Tree", player, world)
Locbtree_name = ["Double Jump Pack", "Map Pack", "Between Trees Sheep", "Hole in the Wall Sheep"]
if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled:
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
Locbtree_name += ["Gun"]
Regbtree.exits = [Entrance(player, "Behind Tree Double Jump", Regbtree),
Entrance(player, "Forest Entrance", Regbtree)]
@ -191,27 +192,27 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue
world.get_entrance("True Double Jump", player).connect(world.get_region("True Double Jump Behind Tree", player))
if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[
Options.Campaign] == Options.Campaign.option_both:
if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign
== Options.Campaign.option_both):
Regfreemiumstart = Region("Freemium Start", player, world)
Locfreemiumstart_name = ["Particles Pack", "Day One Patch Pack", "Checkpoint Pack", "Incredibly Important Pack",
"Nice Try", "Story is Important", "I Get That Reference!"]
if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled:
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
Locfreemiumstart_name += ["Wooden Sword"]
Regfreemiumstart.exits = [Entrance(player, "Vines", Regfreemiumstart)]
Regfreemiumstart.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfreemiumstart)
for loc_name in
Locfreemiumstart_name]
add_coin_freemium(Regfreemiumstart, 50, player)
if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin:
coin_bundle_needed = math.floor(889 / World_Options[Options.CoinSanityRange])
if World_Options.coinsanity == Options.CoinSanity.option_coin:
coin_bundle_needed = math.floor(889 / World_Options.coinbundlequantity)
for i in range(coin_bundle_needed):
item_coin_freemium = f"Live Freemium or Die: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin"
item_coin_freemium = f"Live Freemium or Die: {World_Options.coinbundlequantity * (i + 1)} Coin"
Regfreemiumstart.locations += [
DLCQuestLocation(player, item_coin_freemium, location_table[item_coin_freemium],
Regfreemiumstart)]
if 889 % World_Options[Options.CoinSanityRange] != 0:
if 889 % World_Options.coinbundlequantity != 0:
Regfreemiumstart.locations += [
DLCQuestLocation(player, "Live Freemium or Die: 889 Coin",
location_table["Live Freemium or Die: 889 Coin"],
@ -220,7 +221,7 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue
Regbehindvine = Region("Behind the Vines", player, world)
Locbehindvine_name = ["Wall Jump Pack", "Health Bar Pack", "Parallax Pack"]
if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled:
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
Locbehindvine_name += ["Pickaxe"]
Regbehindvine.exits = [Entrance(player, "Wall Jump Entrance", Regbehindvine)]
Regbehindvine.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regbehindvine) for
@ -260,7 +261,7 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue
Regcutcontent = Region("Cut Content", player, world)
Loccutcontent_name = []
if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled:
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
Loccutcontent_name += ["Humble Indie Bindle"]
Regcutcontent.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regcutcontent) for
loc_name in Loccutcontent_name]
@ -269,7 +270,7 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue
Regnamechange = Region("Name Change", player, world)
Locnamechange_name = []
if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled:
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
Locnamechange_name += ["Box of Various Supplies"]
Regnamechange.exits = [Entrance(player, "Behind Rocks", Regnamechange)]
Regnamechange.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regnamechange) for

View File

@ -1,10 +1,10 @@
import math
import re
from .Locations import DLCQuestLocation
from ..generic.Rules import add_rule, set_rule, item_name_in_locations
from .Items import DLCQuestItem
from BaseClasses import ItemClassification
from worlds.generic.Rules import add_rule, item_name_in_locations, set_rule
from . import Options
from .Items import DLCQuestItem
def create_event(player, event: str):
@ -42,7 +42,7 @@ def set_rules(world, player, World_Options: Options.DLCQuestOptions):
def set_basic_rules(World_Options, has_enough_coin, player, world):
if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die:
if World_Options.campaign == Options.Campaign.option_live_freemium_or_die:
return
set_basic_entrance_rules(player, world)
set_basic_self_obtained_items_rules(World_Options, player, world)
@ -66,12 +66,12 @@ def set_basic_entrance_rules(player, world):
def set_basic_self_obtained_items_rules(World_Options, player, world):
if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_disabled:
if World_Options.item_shuffle != Options.ItemShuffle.option_disabled:
return
set_rule(world.get_entrance("Behind Ogre", player),
lambda state: state.has("Gun Pack", player))
if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required:
if World_Options.time_is_money == Options.TimeIsMoney.option_required:
set_rule(world.get_entrance("Tree", player),
lambda state: state.has("Time is Money Pack", player))
set_rule(world.get_entrance("Cave Tree", player),
@ -87,7 +87,7 @@ def set_basic_self_obtained_items_rules(World_Options, player, world):
def set_basic_shuffled_items_rules(World_Options, player, world):
if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_shuffled:
if World_Options.item_shuffle != Options.ItemShuffle.option_shuffled:
return
set_rule(world.get_entrance("Behind Ogre", player),
lambda state: state.has("Gun", player))
@ -108,13 +108,13 @@ def set_basic_shuffled_items_rules(World_Options, player, world):
set_rule(world.get_location("Gun", player),
lambda state: state.has("Gun Pack", player))
if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required:
if World_Options.time_is_money == Options.TimeIsMoney.option_required:
set_rule(world.get_location("Sword", player),
lambda state: state.has("Time is Money Pack", player))
def set_double_jump_glitchless_rules(World_Options, player, world):
if World_Options[Options.DoubleJumpGlitch] != Options.DoubleJumpGlitch.option_none:
if World_Options.double_jump_glitch != Options.DoubleJumpGlitch.option_none:
return
set_rule(world.get_entrance("Cloud Double Jump", player),
lambda state: state.has("Double Jump Pack", player))
@ -123,7 +123,7 @@ def set_double_jump_glitchless_rules(World_Options, player, world):
def set_easy_double_jump_glitch_rules(World_Options, player, world):
if World_Options[Options.DoubleJumpGlitch] == Options.DoubleJumpGlitch.option_all:
if World_Options.double_jump_glitch == Options.DoubleJumpGlitch.option_all:
return
set_rule(world.get_entrance("Behind Tree Double Jump", player),
lambda state: state.has("Double Jump Pack", player))
@ -132,70 +132,70 @@ def set_easy_double_jump_glitch_rules(World_Options, player, world):
def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world):
if World_Options[Options.CoinSanity] != Options.CoinSanity.option_coin:
if World_Options.coinsanity != Options.CoinSanity.option_coin:
return
number_of_bundle = math.floor(825 / World_Options[Options.CoinSanityRange])
number_of_bundle = math.floor(825 / World_Options.coinbundlequantity)
for i in range(number_of_bundle):
item_coin = f"DLC Quest: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin"
item_coin = f"DLC Quest: {World_Options.coinbundlequantity * (i + 1)} Coin"
set_rule(world.get_location(item_coin, player),
has_enough_coin(player, World_Options[Options.CoinSanityRange] * (i + 1)))
if 825 % World_Options[Options.CoinSanityRange] != 0:
has_enough_coin(player, World_Options.coinbundlequantity * (i + 1)))
if 825 % World_Options.coinbundlequantity != 0:
set_rule(world.get_location("DLC Quest: 825 Coin", player),
has_enough_coin(player, 825))
set_rule(world.get_location("Movement Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(4 / World_Options[Options.CoinSanityRange])))
math.ceil(4 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Animation Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(5 / World_Options[Options.CoinSanityRange])))
math.ceil(5 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Audio Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(5 / World_Options[Options.CoinSanityRange])))
math.ceil(5 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Pause Menu Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(5 / World_Options[Options.CoinSanityRange])))
math.ceil(5 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Time is Money Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(20 / World_Options[Options.CoinSanityRange])))
math.ceil(20 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Double Jump Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(100 / World_Options[Options.CoinSanityRange])))
math.ceil(100 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Pet Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(5 / World_Options[Options.CoinSanityRange])))
math.ceil(5 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Sexy Outfits Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(5 / World_Options[Options.CoinSanityRange])))
math.ceil(5 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Top Hat Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(5 / World_Options[Options.CoinSanityRange])))
math.ceil(5 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Map Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(140 / World_Options[Options.CoinSanityRange])))
math.ceil(140 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Gun Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(75 / World_Options[Options.CoinSanityRange])))
math.ceil(75 / World_Options.coinbundlequantity)))
set_rule(world.get_location("The Zombie Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(5 / World_Options[Options.CoinSanityRange])))
math.ceil(5 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Night Map Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(75 / World_Options[Options.CoinSanityRange])))
math.ceil(75 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Psychological Warfare Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(50 / World_Options[Options.CoinSanityRange])))
math.ceil(50 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Armor for your Horse Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(250 / World_Options[Options.CoinSanityRange])))
math.ceil(250 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Finish the Fight Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
math.ceil(5 / World_Options[Options.CoinSanityRange])))
math.ceil(5 / World_Options.coinbundlequantity)))
def set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world):
if World_Options[Options.CoinSanity] != Options.CoinSanity.option_none:
if World_Options.coinsanity != Options.CoinSanity.option_none:
return
set_rule(world.get_location("Movement Pack", player),
has_enough_coin(player, 4))
@ -232,17 +232,17 @@ def set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player,
def self_basic_win_condition(World_Options, player, world):
if World_Options[Options.EndingChoice] == Options.EndingChoice.option_any:
if World_Options.ending_choice == Options.EndingChoice.option_any:
set_rule(world.get_location("Winning Basic", player),
lambda state: state.has("Finish the Fight Pack", player))
if World_Options[Options.EndingChoice] == Options.EndingChoice.option_true:
if World_Options.ending_choice == Options.EndingChoice.option_true:
set_rule(world.get_location("Winning Basic", player),
lambda state: state.has("Armor for your Horse Pack", player) and state.has("Finish the Fight Pack",
player))
def set_lfod_rules(World_Options, has_enough_coin_freemium, player, world):
if World_Options[Options.Campaign] == Options.Campaign.option_basic:
if World_Options.campaign == Options.Campaign.option_basic:
return
set_lfod_entrance_rules(player, world)
set_boss_door_requirements_rules(player, world)
@ -297,7 +297,7 @@ def set_boss_door_requirements_rules(player, world):
def set_lfod_self_obtained_items_rules(World_Options, player, world):
if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_disabled:
if World_Options.item_shuffle != Options.ItemShuffle.option_disabled:
return
set_rule(world.get_entrance("Vines", player),
lambda state: state.has("Incredibly Important Pack", player))
@ -309,7 +309,7 @@ def set_lfod_self_obtained_items_rules(World_Options, player, world):
def set_lfod_shuffled_items_rules(World_Options, player, world):
if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_shuffled:
if World_Options.item_shuffle != Options.ItemShuffle.option_shuffled:
return
set_rule(world.get_entrance("Vines", player),
lambda state: state.has("Wooden Sword", player) or state.has("Pickaxe", player))
@ -328,79 +328,79 @@ def set_lfod_shuffled_items_rules(World_Options, player, world):
def self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world):
if World_Options[Options.CoinSanity] != Options.CoinSanity.option_coin:
if World_Options.coinsanity != Options.CoinSanity.option_coin:
return
number_of_bundle = math.floor(889 / World_Options[Options.CoinSanityRange])
number_of_bundle = math.floor(889 / World_Options.coinbundlequantity)
for i in range(number_of_bundle):
item_coin_freemium = "Live Freemium or Die: number Coin"
item_coin_loc_freemium = re.sub("number", str(World_Options[Options.CoinSanityRange] * (i + 1)),
item_coin_loc_freemium = re.sub("number", str(World_Options.coinbundlequantity * (i + 1)),
item_coin_freemium)
set_rule(world.get_location(item_coin_loc_freemium, player),
has_enough_coin_freemium(player, World_Options[Options.CoinSanityRange] * (i + 1)))
if 889 % World_Options[Options.CoinSanityRange] != 0:
has_enough_coin_freemium(player, World_Options.coinbundlequantity * (i + 1)))
if 889 % World_Options.coinbundlequantity != 0:
set_rule(world.get_location("Live Freemium or Die: 889 Coin", player),
has_enough_coin_freemium(player, 889))
add_rule(world.get_entrance("Boss Door", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(889 / World_Options[Options.CoinSanityRange])))
math.ceil(889 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Particles Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(5 / World_Options[Options.CoinSanityRange])))
math.ceil(5 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Day One Patch Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(5 / World_Options[Options.CoinSanityRange])))
math.ceil(5 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Checkpoint Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(5 / World_Options[Options.CoinSanityRange])))
math.ceil(5 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Incredibly Important Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(15 / World_Options[Options.CoinSanityRange])))
math.ceil(15 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Wall Jump Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(35 / World_Options[Options.CoinSanityRange])))
math.ceil(35 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Health Bar Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(5 / World_Options[Options.CoinSanityRange])))
math.ceil(5 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Parallax Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(5 / World_Options[Options.CoinSanityRange])))
math.ceil(5 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Harmless Plants Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(130 / World_Options[Options.CoinSanityRange])))
math.ceil(130 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Death of Comedy Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(15 / World_Options[Options.CoinSanityRange])))
math.ceil(15 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Canadian Dialog Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(10 / World_Options[Options.CoinSanityRange])))
math.ceil(10 / World_Options.coinbundlequantity)))
set_rule(world.get_location("DLC NPC Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(15 / World_Options[Options.CoinSanityRange])))
math.ceil(15 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Cut Content Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(40 / World_Options[Options.CoinSanityRange])))
math.ceil(40 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Name Change Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(150 / World_Options[Options.CoinSanityRange])))
math.ceil(150 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Season Pass", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(199 / World_Options[Options.CoinSanityRange])))
math.ceil(199 / World_Options.coinbundlequantity)))
set_rule(world.get_location("High Definition Next Gen Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(20 / World_Options[Options.CoinSanityRange])))
math.ceil(20 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Increased HP Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(10 / World_Options[Options.CoinSanityRange])))
math.ceil(10 / World_Options.coinbundlequantity)))
set_rule(world.get_location("Remove Ads Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
math.ceil(25 / World_Options[Options.CoinSanityRange])))
math.ceil(25 / World_Options.coinbundlequantity)))
def set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world):
if World_Options[Options.CoinSanity] != Options.CoinSanity.option_none:
if World_Options.coinsanity != Options.CoinSanity.option_none:
return
add_rule(world.get_entrance("Boss Door", player),
has_enough_coin_freemium(player, 889))
@ -442,10 +442,10 @@ def set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium,
def set_completion_condition(World_Options, player, world):
if World_Options[Options.Campaign] == Options.Campaign.option_basic:
if World_Options.campaign == Options.Campaign.option_basic:
world.completion_condition[player] = lambda state: state.has("Victory Basic", player)
if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die:
if World_Options.campaign == Options.Campaign.option_live_freemium_or_die:
world.completion_condition[player] = lambda state: state.has("Victory Freemium", player)
if World_Options[Options.Campaign] == Options.Campaign.option_both:
if World_Options.campaign == Options.Campaign.option_both:
world.completion_condition[player] = lambda state: state.has("Victory Basic", player) and state.has(
"Victory Freemium", player)

View File

@ -1,12 +1,13 @@
from typing import Dict, Any, Iterable, Optional, Union
from typing import Union
from BaseClasses import Tutorial
from worlds.AutoWorld import World, WebWorld
from .Items import DLCQuestItem, item_table, ItemData, create_items
from .Locations import location_table, DLCQuestLocation
from .Options import DLCQuest_options, DLCQuestOptions, fetch_options
from .Rules import set_rules
from .Regions import create_regions
from worlds.AutoWorld import WebWorld, World
from . import Options
from .Items import DLCQuestItem, ItemData, create_items, item_table
from .Locations import DLCQuestLocation, location_table
from .Options import DLCQuestOptions
from .Regions import create_regions
from .Rules import set_rules
client_version = 0
@ -35,10 +36,8 @@ class DLCqworld(World):
data_version = 1
option_definitions = DLCQuest_options
def generate_early(self):
self.options = fetch_options(self.multiworld, self.player)
options_dataclass = DLCQuestOptions
options: DLCQuestOptions
def create_regions(self):
create_regions(self.multiworld, self.player, self.options)
@ -68,8 +67,8 @@ class DLCqworld(World):
self.multiworld.itempool.remove(item)
def precollect_coinsanity(self):
if self.options[Options.Campaign] == Options.Campaign.option_basic:
if self.options[Options.CoinSanity] == Options.CoinSanity.option_coin and self.options[Options.CoinSanityRange] >= 5:
if self.options.campaign == Options.Campaign.option_basic:
if self.options.coinsanity == Options.CoinSanity.option_coin and self.options.coinbundlequantity >= 5:
self.multiworld.push_precollected(self.create_item("Movement Pack"))
@ -80,12 +79,11 @@ class DLCqworld(World):
return DLCQuestItem(item.name, item.classification, item.code, self.player)
def fill_slot_data(self):
return {
"death_link": self.multiworld.death_link[self.player].value,
"ending_choice": self.multiworld.ending_choice[self.player].value,
"campaign": self.multiworld.campaign[self.player].value,
"coinsanity": self.multiworld.coinsanity[self.player].value,
"coinbundlerange": self.multiworld.coinbundlequantity[self.player].value,
"item_shuffle": self.multiworld.item_shuffle[self.player].value,
"seed": self.multiworld.per_slot_randoms[self.player].randrange(99999999)
}
options_dict = self.options.as_dict(
"death_link", "ending_choice", "campaign", "coinsanity", "item_shuffle"
)
options_dict.update({
"coinbundlerange": self.options.coinbundlequantity.value,
"seed": self.random.randrange(99999999)
})
return options_dict

View File

@ -4,7 +4,7 @@ from typing import Any, Dict, List, Optional
from BaseClasses import CollectionState, Item, ItemClassification, Tutorial
from worlds.AutoWorld import WebWorld, World
from .constants import ALL_ITEMS, ALWAYS_LOCATIONS, BOSS_LOCATIONS, FILLER, NOTES, PHOBEKINS
from .options import Goal, Logic, NotesNeeded, PowerSeals, messenger_options
from .options import Goal, Logic, MessengerOptions, NotesNeeded, PowerSeals
from .regions import MEGA_SHARDS, REGIONS, REGION_CONNECTIONS, SEALS
from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules
from .shop import FIGURINES, SHOP_ITEMS, shuffle_shop_prices
@ -44,7 +44,8 @@ class MessengerWorld(World):
"Phobekin": set(PHOBEKINS),
}
option_definitions = messenger_options
options_dataclass = MessengerOptions
options: MessengerOptions
base_offset = 0xADD_000
item_name_to_id = {item: item_id
@ -74,9 +75,9 @@ class MessengerWorld(World):
_filler_items: List[str]
def generate_early(self) -> None:
if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
self.multiworld.shuffle_seals[self.player].value = PowerSeals.option_true
self.total_seals = self.multiworld.total_seals[self.player].value
if self.options.goal == Goal.option_power_seal_hunt:
self.options.shuffle_seals.value = PowerSeals.option_true
self.total_seals = self.options.total_seals.value
self.shop_prices, self.figurine_prices = shuffle_shop_prices(self)
@ -87,7 +88,7 @@ class MessengerWorld(World):
def create_items(self) -> None:
# create items that are always in the item pool
itempool = [
itempool: List[MessengerItem] = [
self.create_item(item)
for item in self.item_name_to_id
if item not in
@ -97,13 +98,13 @@ class MessengerWorld(World):
} and "Time Shard" not in item
]
if self.multiworld.goal[self.player] == Goal.option_open_music_box:
if self.options.goal == Goal.option_open_music_box:
# make a list of all notes except those in the player's defined starting inventory, and adjust the
# amount we need to put in the itempool and precollect based on that
notes = [note for note in NOTES if note not in self.multiworld.precollected_items[self.player]]
self.random.shuffle(notes)
precollected_notes_amount = NotesNeeded.range_end - \
self.multiworld.notes_needed[self.player] - \
self.options.notes_needed - \
(len(NOTES) - len(notes))
if precollected_notes_amount:
for note in notes[:precollected_notes_amount]:
@ -111,15 +112,14 @@ class MessengerWorld(World):
notes = notes[precollected_notes_amount:]
itempool += [self.create_item(note) for note in notes]
elif self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
elif self.options.goal == Goal.option_power_seal_hunt:
total_seals = min(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool),
self.multiworld.total_seals[self.player].value)
self.options.total_seals.value)
if total_seals < self.total_seals:
logging.warning(f"Not enough locations for total seals setting "
f"({self.multiworld.total_seals[self.player].value}). Adjusting to {total_seals}")
f"({self.options.total_seals}). Adjusting to {total_seals}")
self.total_seals = total_seals
self.required_seals =\
int(self.multiworld.percent_seals_required[self.player].value / 100 * self.total_seals)
self.required_seals = int(self.options.percent_seals_required.value / 100 * self.total_seals)
seals = [self.create_item("Power Seal") for _ in range(self.total_seals)]
for i in range(self.required_seals):
@ -138,7 +138,7 @@ class MessengerWorld(World):
self.multiworld.itempool += itempool
def set_rules(self) -> None:
logic = self.multiworld.logic_level[self.player]
logic = self.options.logic_level
if logic == Logic.option_normal:
MessengerRules(self).set_messenger_rules()
elif logic == Logic.option_hard:
@ -151,12 +151,12 @@ class MessengerWorld(World):
figure_prices = {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()}
return {
"deathlink": self.multiworld.death_link[self.player].value,
"goal": self.multiworld.goal[self.player].current_key,
"music_box": self.multiworld.music_box[self.player].value,
"deathlink": self.options.death_link.value,
"goal": self.options.goal.current_key,
"music_box": self.options.music_box.value,
"required_seals": self.required_seals,
"mega_shards": self.multiworld.shuffle_shards[self.player].value,
"logic": self.multiworld.logic_level[self.player].current_key,
"mega_shards": self.options.shuffle_shards.value,
"logic": self.options.logic_level.current_key,
"shop": shop_prices,
"figures": figure_prices,
"max_price": self.total_shards,
@ -175,7 +175,7 @@ class MessengerWorld(World):
item_id: Optional[int] = self.item_name_to_id.get(name, None)
override_prog = getattr(self, "multiworld") is not None and \
name in {"Windmill Shuriken"} and \
self.multiworld.logic_level[self.player] > Logic.option_normal
self.options.logic_level > Logic.option_normal
count = 0
if "Time Shard " in name:
count = int(name.strip("Time Shard ()"))

View File

@ -1,7 +1,7 @@
from .shop import FIGURINES, SHOP_ITEMS
# items
# listing individual groups first for easy lookup
from .shop import SHOP_ITEMS, FIGURINES
NOTES = [
"Key of Hope",
"Key of Chaos",

View File

@ -1,7 +1,10 @@
from dataclasses import dataclass
from typing import Dict
from schema import Schema, Or, And, Optional
from Options import DefaultOnToggle, DeathLink, Range, Accessibility, Choice, Toggle, OptionDict, StartInventoryPool
from schema import And, Optional, Or, Schema
from Options import Accessibility, Choice, DeathLink, DefaultOnToggle, OptionDict, PerGameCommonOptions, Range, \
StartInventoryPool, Toggle
class MessengerAccessibility(Accessibility):
@ -129,18 +132,19 @@ class PlannedShopPrices(OptionDict):
})
messenger_options = {
"accessibility": MessengerAccessibility,
"start_inventory": StartInventoryPool,
"logic_level": Logic,
"shuffle_seals": PowerSeals,
"shuffle_shards": MegaShards,
"goal": Goal,
"music_box": MusicBox,
"notes_needed": NotesNeeded,
"total_seals": AmountSeals,
"percent_seals_required": RequiredSeals,
"shop_price": ShopPrices,
"shop_price_plan": PlannedShopPrices,
"death_link": DeathLink,
}
@dataclass
class MessengerOptions(PerGameCommonOptions):
accessibility: MessengerAccessibility
start_inventory: StartInventoryPool
logic_level: Logic
shuffle_seals: PowerSeals
shuffle_shards: MegaShards
goal: Goal
music_box: MusicBox
notes_needed: NotesNeeded
total_seals: AmountSeals
percent_seals_required: RequiredSeals
shop_price: ShopPrices
shop_price_plan: PlannedShopPrices
death_link: DeathLink

View File

@ -1,4 +1,4 @@
from typing import Dict, Set, List
from typing import Dict, List, Set
REGIONS: Dict[str, List[str]] = {
"Menu": [],

View File

@ -1,9 +1,9 @@
from typing import Dict, Callable, TYPE_CHECKING
from typing import Callable, Dict, TYPE_CHECKING
from BaseClasses import CollectionState, MultiWorld
from worlds.generic.Rules import set_rule, allow_self_locking_items, add_rule
from .options import MessengerAccessibility, Goal
from BaseClasses import CollectionState
from worlds.generic.Rules import add_rule, allow_self_locking_items, set_rule
from .constants import NOTES, PHOBEKINS
from .options import Goal, MessengerAccessibility
from .subclasses import MessengerShopLocation
if TYPE_CHECKING:
@ -145,13 +145,13 @@ class MessengerRules:
if region.name == "The Shop":
for loc in [location for location in region.locations if isinstance(location, MessengerShopLocation)]:
loc.access_rule = loc.can_afford
if multiworld.goal[self.player] == Goal.option_power_seal_hunt:
if self.world.options.goal == Goal.option_power_seal_hunt:
set_rule(multiworld.get_entrance("Tower HQ -> Music Box", self.player),
lambda state: state.has("Shop Chest", self.player))
multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player)
if multiworld.accessibility[self.player] > MessengerAccessibility.option_locations:
set_self_locking_items(multiworld, self.player)
set_self_locking_items(self.world, self.player)
class MessengerHardRules(MessengerRules):
@ -212,9 +212,9 @@ class MessengerHardRules(MessengerRules):
def set_messenger_rules(self) -> None:
super().set_messenger_rules()
for loc, rule in self.extra_rules.items():
if not self.world.multiworld.shuffle_seals[self.player] and "Seal" in loc:
if not self.world.options.shuffle_seals and "Seal" in loc:
continue
if not self.world.multiworld.shuffle_shards[self.player] and "Shard" in loc:
if not self.world.options.shuffle_shards and "Shard" in loc:
continue
add_rule(self.world.multiworld.get_location(loc, self.player), rule, "or")
@ -249,20 +249,22 @@ class MessengerOOBRules(MessengerRules):
def set_messenger_rules(self) -> None:
super().set_messenger_rules()
self.world.multiworld.completion_condition[self.player] = lambda state: True
self.world.multiworld.accessibility[self.player].value = MessengerAccessibility.option_minimal
self.world.options.accessibility.value = MessengerAccessibility.option_minimal
def set_self_locking_items(multiworld: MultiWorld, player: int) -> None:
def set_self_locking_items(world: MessengerWorld, player: int) -> None:
multiworld = world.multiworld
# do the ones for seal shuffle on and off first
allow_self_locking_items(multiworld.get_location("Searing Crags - Key of Strength", player), "Power Thistle")
allow_self_locking_items(multiworld.get_location("Sunken Shrine - Key of Love", player), "Sun Crest", "Moon Crest")
allow_self_locking_items(multiworld.get_location("Corrupted Future - Key of Courage", player), "Demon King Crown")
# add these locations when seals are shuffled
if multiworld.shuffle_seals[player]:
if world.options.shuffle_seals:
allow_self_locking_items(multiworld.get_location("Elemental Skylands Seal - Water", player), "Currents Master")
# add these locations when seals and shards aren't shuffled
elif not multiworld.shuffle_shards[player]:
elif not world.options.shuffle_shards:
for entrance in multiworld.get_region("Cloud Ruins", player).entrances:
entrance.access_rule = lambda state: state.has("Wingsuit", player) or state.has("Rope Dart", player)
allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS)

View File

@ -74,8 +74,8 @@ FIGURINES: Dict[str, ShopData] = {
def shuffle_shop_prices(world: MessengerWorld) -> Tuple[Dict[str, int], Dict[str, int]]:
shop_price_mod = world.multiworld.shop_price[world.player].value
shop_price_planned = world.multiworld.shop_price_plan[world.player]
shop_price_mod = world.options.shop_price.value
shop_price_planned = world.options.shop_price_plan
shop_prices: Dict[str, int] = {}
figurine_prices: Dict[str, int] = {}

View File

@ -9,16 +9,15 @@ from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS
if TYPE_CHECKING:
from . import MessengerWorld
else:
MessengerWorld = object
class MessengerRegion(Region):
def __init__(self, name: str, world: MessengerWorld) -> None:
def __init__(self, name: str, world: "MessengerWorld") -> None:
super().__init__(name, world.player, world.multiworld)
locations = [loc for loc in REGIONS[self.name]]
if self.name == "The Shop":
if self.multiworld.goal[self.player] > Goal.option_open_music_box:
if world.options.goal > Goal.option_open_music_box:
locations.append("Shop Chest")
shop_locations = {f"The Shop - {shop_loc}": world.location_name_to_id[f"The Shop - {shop_loc}"]
for shop_loc in SHOP_ITEMS}
@ -26,9 +25,9 @@ class MessengerRegion(Region):
self.add_locations(shop_locations, MessengerShopLocation)
elif self.name == "Tower HQ":
locations.append("Money Wrench")
if self.multiworld.shuffle_seals[self.player] and self.name in SEALS:
if world.options.shuffle_seals and self.name in SEALS:
locations += [seal_loc for seal_loc in SEALS[self.name]]
if self.multiworld.shuffle_shards[self.player] and self.name in MEGA_SHARDS:
if world.options.shuffle_shards and self.name in MEGA_SHARDS:
locations += [shard for shard in MEGA_SHARDS[self.name]]
loc_dict = {loc: world.location_name_to_id[loc] if loc in world.location_name_to_id else None
for loc in locations}
@ -49,7 +48,7 @@ class MessengerShopLocation(MessengerLocation):
@cached_property
def cost(self) -> int:
name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped
world: MessengerWorld = self.parent_region.multiworld.worlds[self.player]
world = cast("MessengerWorld", self.parent_region.multiworld.worlds[self.player])
# short circuit figurines which all require demon's bane be purchased, but nothing else
if "Figurine" in name:
return world.figurine_prices[name] +\
@ -70,9 +69,8 @@ class MessengerShopLocation(MessengerLocation):
return world.shop_prices[name]
def can_afford(self, state: CollectionState) -> bool:
world: MessengerWorld = state.multiworld.worlds[self.player]
cost = self.cost
can_afford = state.has("Shards", self.player, min(cost, world.total_shards))
world = cast("MessengerWorld", state.multiworld.worlds[self.player])
can_afford = state.has("Shards", self.player, min(self.cost, world.total_shards))
if "Figurine" in self.name:
can_afford = state.has("Money Wrench", self.player) and can_afford\
and state.can_reach("Money Wrench", "Location", self.player)

View File

@ -32,7 +32,7 @@ from .Cosmetics import patch_cosmetics
from Utils import get_options
from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType
from Options import Range, Toggle, VerifyKeys
from Options import Range, Toggle, VerifyKeys, Accessibility
from Fill import fill_restrictive, fast_fill, FillError
from worlds.generic.Rules import exclusion_rules, add_item_rule
from ..AutoWorld import World, AutoLogicRegister, WebWorld
@ -286,7 +286,7 @@ class OOTWorld(World):
# No Logic forces all tricks on, prog balancing off and beatable-only
elif self.logic_rules == 'no_logic':
self.multiworld.progression_balancing[self.player].value = False
self.multiworld.accessibility[self.player] = self.multiworld.accessibility[self.player].from_text("minimal")
self.multiworld.accessibility[self.player].value = Accessibility.option_minimal
for trick in normalized_name_tricks.values():
setattr(self, trick['name'], True)

View File

@ -1,6 +1,7 @@
from dataclasses import dataclass
from enum import IntEnum
from typing import TypedDict
from Options import DefaultOnToggle, Toggle, Range, Choice, OptionSet
from Options import DefaultOnToggle, PerGameCommonOptions, Toggle, Range, Choice, OptionSet
from .Overcooked2Levels import Overcooked2Dlc
class LocationBalancingMode(IntEnum):
@ -167,32 +168,30 @@ class StarThresholdScale(Range):
default = 35
overcooked_options = {
@dataclass
class OC2Options(PerGameCommonOptions):
# generator options
"location_balancing": LocationBalancing,
"ramp_tricks": RampTricks,
location_balancing: LocationBalancing
ramp_tricks: RampTricks
# deathlink
"deathlink": DeathLink,
deathlink: DeathLink
# randomization options
"shuffle_level_order": ShuffleLevelOrder,
"include_dlcs": DLCOptionSet,
"include_horde_levels": IncludeHordeLevels,
"prep_levels": PrepLevels,
"kevin_levels": KevinLevels,
shuffle_level_order: ShuffleLevelOrder
include_dlcs: DLCOptionSet
include_horde_levels: IncludeHordeLevels
prep_levels: PrepLevels
kevin_levels: KevinLevels
# quality of life options
"fix_bugs": FixBugs,
"shorter_level_duration": ShorterLevelDuration,
"short_horde_levels": ShortHordeLevels,
"always_preserve_cooking_progress": AlwaysPreserveCookingProgress,
"always_serve_oldest_order": AlwaysServeOldestOrder,
"display_leaderboard_scores": DisplayLeaderboardScores,
fix_bugs: FixBugs
shorter_level_duration: ShorterLevelDuration
short_horde_levels: ShortHordeLevels
always_preserve_cooking_progress: AlwaysPreserveCookingProgress
always_serve_oldest_order: AlwaysServeOldestOrder
display_leaderboard_scores: DisplayLeaderboardScores
# difficulty settings
"stars_to_win": StarsToWin,
"star_threshold_scale": StarThresholdScale,
}
OC2Options = TypedDict("OC2Options", {option.__name__: option for option in overcooked_options.values()})
stars_to_win: StarsToWin
star_threshold_scale: StarThresholdScale

View File

@ -6,7 +6,7 @@ from worlds.AutoWorld import World, WebWorld
from .Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, Overcooked2GenericLevel
from .Locations import Overcooked2Location, oc2_location_name_to_id, oc2_location_id_to_name
from .Options import overcooked_options, OC2Options, OC2OnToggle, LocationBalancingMode, DeathLinkMode
from .Options import OC2Options, OC2OnToggle, LocationBalancingMode, DeathLinkMode
from .Items import item_table, Overcooked2Item, item_name_to_id, item_id_to_name, item_to_unlock_event, item_frequencies, dlc_exclusives
from .Logic import has_requirements_for_level_star, has_requirements_for_level_access, level_shuffle_factory, is_item_progression, is_useful
@ -47,7 +47,6 @@ class Overcooked2World(World):
game = "Overcooked! 2"
web = Overcooked2Web()
required_client_version = (0, 3, 8)
option_definitions = overcooked_options
topology_present: bool = False
data_version = 3
@ -57,13 +56,14 @@ class Overcooked2World(World):
location_id_to_name = oc2_location_id_to_name
location_name_to_id = oc2_location_name_to_id
options: Dict[str, Any]
options_dataclass = OC2Options
options: OC2Options
itempool: List[Overcooked2Item]
# Helper Functions
def is_level_horde(self, level_id: int) -> bool:
return self.options["IncludeHordeLevels"] and \
return self.options.include_horde_levels and \
(self.level_mapping is not None) and \
level_id in self.level_mapping.keys() and \
self.level_mapping[level_id].is_horde
@ -145,11 +145,6 @@ class Overcooked2World(World):
location
)
def get_options(self) -> Dict[str, Any]:
return OC2Options({option.__name__: getattr(self.multiworld, name)[self.player].result
if issubclass(option, OC2OnToggle) else getattr(self.multiworld, name)[self.player].value
for name, option in overcooked_options.items()})
def get_n_random_locations(self, n: int) -> List[int]:
"""Return a list of n random non-repeating level locations"""
levels = list()
@ -160,7 +155,7 @@ class Overcooked2World(World):
for level in Overcooked2Level():
if level.level_id == 36:
continue
elif not self.options["KevinLevels"] and level.level_id > 36:
elif not self.options.kevin_levels and level.level_id > 36:
break
levels.append(level.level_id)
@ -231,26 +226,25 @@ class Overcooked2World(World):
def generate_early(self):
self.player_name = self.multiworld.player_name[self.player]
self.options = self.get_options()
# 0.0 to 1.0 where 1.0 is World Record
self.star_threshold_scale = self.options["StarThresholdScale"] / 100.0
self.star_threshold_scale = self.options.star_threshold_scale / 100.0
# Parse DLCOptionSet back into enums
self.enabled_dlc = {Overcooked2Dlc(x) for x in self.options["DLCOptionSet"]}
self.enabled_dlc = {Overcooked2Dlc(x) for x in self.options.include_dlcs.value}
# Generate level unlock requirements such that the levels get harder to unlock
# the further the game has progressed, and levels progress radially rather than linearly
self.level_unlock_counts = level_unlock_requirement_factory(self.options["StarsToWin"])
self.level_unlock_counts = level_unlock_requirement_factory(self.options.stars_to_win.value)
# Assign new kitchens to each spot on the overworld using pure random chance and nothing else
if self.options["ShuffleLevelOrder"]:
if self.options.shuffle_level_order:
self.level_mapping = \
level_shuffle_factory(
self.multiworld.random,
self.options["PrepLevels"] != PrepLevelMode.excluded,
self.options["IncludeHordeLevels"],
self.options["KevinLevels"],
self.options.prep_levels != PrepLevelMode.excluded,
self.options.include_horde_levels.result,
self.options.kevin_levels.result,
self.enabled_dlc,
self.player_name,
)
@ -277,7 +271,7 @@ class Overcooked2World(World):
# Create and populate "regions" (a.k.a. levels)
for level in Overcooked2Level():
if not self.options["KevinLevels"] and level.level_id > 36:
if not self.options.kevin_levels and level.level_id > 36:
break
# Create Region (e.g. "1-1")
@ -336,7 +330,7 @@ class Overcooked2World(World):
level_access_rule: Callable[[CollectionState], bool] = \
lambda state, level_name=level.level_name, previous_level_completed_event_name=previous_level_completed_event_name, required_star_count=required_star_count: \
has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.options["RampTricks"], self.player)
has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.options.ramp_tricks.result, self.player)
self.connect_regions("Overworld", level.level_name, level_access_rule)
# Level --> Overworld
@ -369,11 +363,11 @@ class Overcooked2World(World):
# Item is always useless with these settings
continue
if not self.options["IncludeHordeLevels"] and item_name in ["Calmer Unbread", "Coin Purse"]:
if not self.options.include_horde_levels and item_name in ["Calmer Unbread", "Coin Purse"]:
# skip horde-specific items if no horde levels
continue
if not self.options["KevinLevels"]:
if not self.options.kevin_levels:
if item_name.startswith("Kevin"):
# skip kevin items if no kevin levels
continue
@ -382,7 +376,7 @@ class Overcooked2World(World):
# skip dark green ramp if there's no Kevin-1 to reveal it
continue
if is_item_progression(item_name, self.level_mapping, self.options["KevinLevels"]):
if is_item_progression(item_name, self.level_mapping, self.options.kevin_levels):
# progression.append(item_name)
classification = ItemClassification.progression
else:
@ -404,7 +398,7 @@ class Overcooked2World(World):
# Fill any free space with filler
pool_count = len(oc2_location_name_to_id)
if not self.options["KevinLevels"]:
if not self.options.kevin_levels:
pool_count -= 8
while len(self.itempool) < pool_count:
@ -416,7 +410,7 @@ class Overcooked2World(World):
def place_events(self):
# Add Events (Star Acquisition)
for level in Overcooked2Level():
if not self.options["KevinLevels"] and level.level_id > 36:
if not self.options.kevin_levels and level.level_id > 36:
break
if level.level_id != 36:
@ -449,7 +443,7 @@ class Overcooked2World(World):
# Serialize Level Order
story_level_order = dict()
if self.options["ShuffleLevelOrder"]:
if self.options.shuffle_level_order:
for level_id in self.level_mapping:
level: Overcooked2GenericLevel = self.level_mapping[level_id]
story_level_order[str(level_id)] = {
@ -481,7 +475,7 @@ class Overcooked2World(World):
level_unlock_requirements[str(level_id)] = level_id - 1
# Set Kevin Unlock Requirements
if self.options["KevinLevels"]:
if self.options.kevin_levels:
def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]:
location = self.multiworld.find_item(f"Kevin-{level_id-36}", self.player)
if location.player != self.player:
@ -506,7 +500,7 @@ class Overcooked2World(World):
on_level_completed[level_id] = [item_to_unlock_event(location.item.name)]
# Put it all together
star_threshold_scale = self.options["StarThresholdScale"] / 100
star_threshold_scale = self.options.star_threshold_scale / 100
base_data = {
# Changes Inherent to rando
@ -528,13 +522,13 @@ class Overcooked2World(World):
"SaveFolderName": mod_name,
"CustomOrderTimeoutPenalty": 10,
"LevelForceHide": [37, 38, 39, 40, 41, 42, 43, 44],
"LocalDeathLink": self.options["DeathLink"] != DeathLinkMode.disabled,
"BurnTriggersDeath": self.options["DeathLink"] == DeathLinkMode.death_and_overcook,
"LocalDeathLink": self.options.deathlink != DeathLinkMode.disabled,
"BurnTriggersDeath": self.options.deathlink == DeathLinkMode.death_and_overcook,
# Game Modifications
"LevelPurchaseRequirements": level_purchase_requirements,
"Custom66TimerScale": max(0.4, 0.25 + (1.0 - star_threshold_scale)*0.6),
"ShortHordeLevels": self.options["ShortHordeLevels"],
"ShortHordeLevels": self.options.short_horde_levels,
"CustomLevelOrder": custom_level_order,
# Items (Starting Inventory)
@ -580,28 +574,27 @@ class Overcooked2World(World):
# Set remaining data in the options dict
bugs = ["FixDoubleServing", "FixSinkBug", "FixControlStickThrowBug", "FixEmptyBurnerThrow"]
for bug in bugs:
self.options[bug] = self.options["FixBugs"]
self.options["PreserveCookingProgress"] = self.options["AlwaysPreserveCookingProgress"]
self.options["TimerAlwaysStarts"] = self.options["PrepLevels"] == PrepLevelMode.ayce
self.options["LevelTimerScale"] = 0.666 if self.options["ShorterLevelDuration"] else 1.0
self.options["LeaderboardScoreScale"] = {
base_data[bug] = self.options.fix_bugs.result
base_data["PreserveCookingProgress"] = self.options.always_preserve_cooking_progress.result
base_data["TimerAlwaysStarts"] = self.options.prep_levels == PrepLevelMode.ayce
base_data["LevelTimerScale"] = 0.666 if self.options.shorter_level_duration else 1.0
base_data["LeaderboardScoreScale"] = {
"FourStars": 1.0,
"ThreeStars": star_threshold_scale,
"TwoStars": star_threshold_scale * 0.75,
"OneStar": star_threshold_scale * 0.35,
}
base_data.update(self.options)
return base_data
def fill_slot_data(self) -> Dict[str, Any]:
return self.fill_json_data()
def write_spoiler(self, spoiler_handle: TextIO) -> None:
if not self.options["ShuffleLevelOrder"]:
if not self.options.shuffle_level_order:
return
world: Overcooked2World = self.multiworld.worlds[self.player]
world: Overcooked2World = self
spoiler_handle.write(f"\n\n{self.player_name}'s Level Order:\n\n")
for overworld_id in world.level_mapping:
overworld_name = Overcooked2GenericLevel(overworld_id).shortname.split("Story ")[1]

View File

@ -1,5 +1,5 @@
from typing import Dict
from Options import Option, Toggle, DefaultOnToggle, DeathLink, Range, Choice
from dataclasses import dataclass
from Options import Toggle, DefaultOnToggle, DeathLink, Range, Choice, PerGameCommonOptions
# NOTE be aware that since the range of item ids that RoR2 uses is based off of the maximums of checks
@ -274,39 +274,40 @@ class ItemWeights(Choice):
option_void = 9
# define a dictionary for the weights of the generated item pool.
ror2_weights: Dict[str, type(Option)] = {
"green_scrap": GreenScrap,
"red_scrap": RedScrap,
"yellow_scrap": YellowScrap,
"white_scrap": WhiteScrap,
"common_item": CommonItem,
"uncommon_item": UncommonItem,
"legendary_item": LegendaryItem,
"boss_item": BossItem,
"lunar_item": LunarItem,
"void_item": VoidItem,
"equipment": Equipment
}
ror2_options: Dict[str, type(Option)] = {
"goal": Goal,
"total_locations": TotalLocations,
"chests_per_stage": ChestsPerEnvironment,
"shrines_per_stage": ShrinesPerEnvironment,
"scavengers_per_stage": ScavengersPerEnvironment,
"scanner_per_stage": ScannersPerEnvironment,
"altars_per_stage": AltarsPerEnvironment,
"total_revivals": TotalRevivals,
"start_with_revive": StartWithRevive,
"final_stage_death": FinalStageDeath,
"begin_with_loop": BeginWithLoop,
"dlc_sotv": DLC_SOTV,
"death_link": DeathLink,
"item_pickup_step": ItemPickupStep,
"shrine_use_step": ShrineUseStep,
"enable_lunar": AllowLunarItems,
"item_weights": ItemWeights,
"item_pool_presets": ItemPoolPresetToggle,
**ror2_weights
}
# define a class for the weights of the generated item pool.
@dataclass
class ROR2Weights:
green_scrap: GreenScrap
red_scrap: RedScrap
yellow_scrap: YellowScrap
white_scrap: WhiteScrap
common_item: CommonItem
uncommon_item: UncommonItem
legendary_item: LegendaryItem
boss_item: BossItem
lunar_item: LunarItem
void_item: VoidItem
equipment: Equipment
@dataclass
class ROR2Options(PerGameCommonOptions, ROR2Weights):
goal: Goal
total_locations: TotalLocations
chests_per_stage: ChestsPerEnvironment
shrines_per_stage: ShrinesPerEnvironment
scavengers_per_stage: ScavengersPerEnvironment
scanner_per_stage: ScannersPerEnvironment
altars_per_stage: AltarsPerEnvironment
total_revivals: TotalRevivals
start_with_revive: StartWithRevive
final_stage_death: FinalStageDeath
begin_with_loop: BeginWithLoop
dlc_sotv: DLC_SOTV
death_link: DeathLink
item_pickup_step: ItemPickupStep
shrine_use_step: ShrineUseStep
enable_lunar: AllowLunarItems
item_weights: ItemWeights
item_pool_presets: ItemPoolPresetToggle

View File

@ -6,7 +6,7 @@ from .Rules import set_rules
from .RoR2Environments import *
from BaseClasses import Region, Entrance, Item, ItemClassification, MultiWorld, Tutorial
from .Options import ror2_options, ItemWeights
from .Options import ItemWeights, ROR2Options
from worlds.AutoWorld import World, WebWorld
from .Regions import create_regions
@ -28,8 +28,9 @@ class RiskOfRainWorld(World):
Combine loot in surprising ways and master each character until you become the havoc you feared upon your
first crash landing.
"""
game: str = "Risk of Rain 2"
option_definitions = ror2_options
game = "Risk of Rain 2"
options_dataclass = ROR2Options
options: ROR2Options
topology_present = False
item_name_to_id = item_table
@ -46,45 +47,44 @@ class RiskOfRainWorld(World):
def generate_early(self) -> None:
# figure out how many revivals should exist in the pool
if self.multiworld.goal[self.player] == "classic":
total_locations = self.multiworld.total_locations[self.player].value
if self.options.goal == "classic":
total_locations = self.options.total_locations.value
else:
total_locations = len(
orderedstage_location.get_locations(
chests=self.multiworld.chests_per_stage[self.player].value,
shrines=self.multiworld.shrines_per_stage[self.player].value,
scavengers=self.multiworld.scavengers_per_stage[self.player].value,
scanners=self.multiworld.scanner_per_stage[self.player].value,
altars=self.multiworld.altars_per_stage[self.player].value,
dlc_sotv=self.multiworld.dlc_sotv[self.player].value
chests=self.options.chests_per_stage.value,
shrines=self.options.shrines_per_stage.value,
scavengers=self.options.scavengers_per_stage.value,
scanners=self.options.scanner_per_stage.value,
altars=self.options.altars_per_stage.value,
dlc_sotv=self.options.dlc_sotv.value
)
)
self.total_revivals = int(self.multiworld.total_revivals[self.player].value / 100 *
self.total_revivals = int(self.options.total_revivals.value / 100 *
total_locations)
# self.total_revivals = self.multiworld.total_revivals[self.player].value
if self.multiworld.start_with_revive[self.player].value:
if self.options.start_with_revive:
self.total_revivals -= 1
def create_items(self) -> None:
# shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend
if self.multiworld.start_with_revive[self.player]:
if self.options.start_with_revive:
self.multiworld.push_precollected(self.multiworld.create_item("Dio's Best Friend", self.player))
environments_pool = {}
# only mess with the environments if they are set as items
if self.multiworld.goal[self.player] == "explore":
if self.options.goal == "explore":
# figure out all available ordered stages for each tier
environment_available_orderedstages_table = environment_vanilla_orderedstages_table
if self.multiworld.dlc_sotv[self.player]:
if self.options.dlc_sotv:
environment_available_orderedstages_table = collapse_dict_list_vertical(environment_available_orderedstages_table, environment_sotv_orderedstages_table)
environments_pool = shift_by_offset(environment_vanilla_table, environment_offest)
if self.multiworld.dlc_sotv[self.player]:
if self.options.dlc_sotv:
environment_offset_table = shift_by_offset(environment_sotv_table, environment_offest)
environments_pool = {**environments_pool, **environment_offset_table}
environments_to_precollect = 5 if self.multiworld.begin_with_loop[self.player].value else 1
environments_to_precollect = 5 if self.options.begin_with_loop else 1
# percollect environments for each stage (or just stage 1)
for i in range(environments_to_precollect):
unlock = self.multiworld.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1)
@ -100,19 +100,19 @@ class RiskOfRainWorld(World):
for env_name, _ in environments_pool.items():
itempool += [env_name]
if self.multiworld.goal[self.player] == "classic":
if self.options.goal == "classic":
# classic mode
total_locations = self.multiworld.total_locations[self.player].value
total_locations = self.options.total_locations.value
else:
# explore mode
total_locations = len(
orderedstage_location.get_locations(
chests=self.multiworld.chests_per_stage[self.player].value,
shrines=self.multiworld.shrines_per_stage[self.player].value,
scavengers=self.multiworld.scavengers_per_stage[self.player].value,
scanners=self.multiworld.scanner_per_stage[self.player].value,
altars=self.multiworld.altars_per_stage[self.player].value,
dlc_sotv=self.multiworld.dlc_sotv[self.player].value
chests=self.options.chests_per_stage.value,
shrines=self.options.shrines_per_stage.value,
scavengers=self.options.scavengers_per_stage.value,
scanners=self.options.scanner_per_stage.value,
altars=self.options.altars_per_stage.value,
dlc_sotv=self.options.dlc_sotv.value
)
)
# Create junk items
@ -138,9 +138,9 @@ class RiskOfRainWorld(World):
def create_junk_pool(self) -> Dict:
# if presets are enabled generate junk_pool from the selected preset
pool_option = self.multiworld.item_weights[self.player].value
pool_option = self.options.item_weights.value
junk_pool: Dict[str, int] = {}
if self.multiworld.item_pool_presets[self.player]:
if self.options.item_pool_presets:
# generate chaos weights if the preset is chosen
if pool_option == ItemWeights.option_chaos:
for name, max_value in item_pool_weights[pool_option].items():
@ -149,31 +149,31 @@ class RiskOfRainWorld(World):
junk_pool = item_pool_weights[pool_option].copy()
else: # generate junk pool from user created presets
junk_pool = {
"Item Scrap, Green": self.multiworld.green_scrap[self.player].value,
"Item Scrap, Red": self.multiworld.red_scrap[self.player].value,
"Item Scrap, Yellow": self.multiworld.yellow_scrap[self.player].value,
"Item Scrap, White": self.multiworld.white_scrap[self.player].value,
"Common Item": self.multiworld.common_item[self.player].value,
"Uncommon Item": self.multiworld.uncommon_item[self.player].value,
"Legendary Item": self.multiworld.legendary_item[self.player].value,
"Boss Item": self.multiworld.boss_item[self.player].value,
"Lunar Item": self.multiworld.lunar_item[self.player].value,
"Void Item": self.multiworld.void_item[self.player].value,
"Equipment": self.multiworld.equipment[self.player].value
"Item Scrap, Green": self.options.green_scrap.value,
"Item Scrap, Red": self.options.red_scrap.value,
"Item Scrap, Yellow": self.options.yellow_scrap.value,
"Item Scrap, White": self.options.white_scrap.value,
"Common Item": self.options.common_item.value,
"Uncommon Item": self.options.uncommon_item.value,
"Legendary Item": self.options.legendary_item.value,
"Boss Item": self.options.boss_item.value,
"Lunar Item": self.options.lunar_item.value,
"Void Item": self.options.void_item.value,
"Equipment": self.options.equipment.value
}
# remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled
if not (self.multiworld.enable_lunar[self.player] or pool_option == ItemWeights.option_lunartic):
if not (self.options.enable_lunar or pool_option == ItemWeights.option_lunartic):
junk_pool.pop("Lunar Item")
# remove void items from the pool
if not (self.multiworld.dlc_sotv[self.player] or pool_option == ItemWeights.option_void):
if not (self.options.dlc_sotv or pool_option == ItemWeights.option_void):
junk_pool.pop("Void Item")
return junk_pool
def create_regions(self) -> None:
if self.multiworld.goal[self.player] == "classic":
if self.options.goal == "classic":
# classic mode
menu = create_region(self.multiworld, self.player, "Menu")
self.multiworld.regions.append(menu)
@ -182,7 +182,7 @@ class RiskOfRainWorld(World):
victory_region = create_region(self.multiworld, self.player, "Victory")
self.multiworld.regions.append(victory_region)
petrichor = create_region(self.multiworld, self.player, "Petrichor V",
get_classic_item_pickups(self.multiworld.total_locations[self.player].value))
get_classic_item_pickups(self.options.total_locations.value))
self.multiworld.regions.append(petrichor)
# classic mode can get to victory from the beginning of the game
@ -200,21 +200,13 @@ class RiskOfRainWorld(World):
create_events(self.multiworld, self.player)
def fill_slot_data(self):
options_dict = self.options.as_dict("item_pickup_step", "shrine_use_step", "goal", "total_locations",
"chests_per_stage", "shrines_per_stage", "scavengers_per_stage",
"scanner_per_stage", "altars_per_stage", "total_revivals", "start_with_revive",
"final_stage_death", "death_link", casing="camel")
return {
"itemPickupStep": self.multiworld.item_pickup_step[self.player].value,
"shrineUseStep": self.multiworld.shrine_use_step[self.player].value,
"goal": self.multiworld.goal[self.player].value,
**options_dict,
"seed": "".join(self.multiworld.per_slot_randoms[self.player].choice(string.digits) for _ in range(16)),
"totalLocations": self.multiworld.total_locations[self.player].value,
"chestsPerStage": self.multiworld.chests_per_stage[self.player].value,
"shrinesPerStage": self.multiworld.shrines_per_stage[self.player].value,
"scavengersPerStage": self.multiworld.scavengers_per_stage[self.player].value,
"scannerPerStage": self.multiworld.scanner_per_stage[self.player].value,
"altarsPerStage": self.multiworld.altars_per_stage[self.player].value,
"totalRevivals": self.multiworld.total_revivals[self.player].value,
"startWithDio": self.multiworld.start_with_revive[self.player].value,
"finalStageDeath": self.multiworld.final_stage_death[self.player].value,
"deathLink": self.multiworld.death_link[self.player].value,
}
def create_item(self, name: str) -> Item:
@ -241,12 +233,12 @@ class RiskOfRainWorld(World):
def create_events(world: MultiWorld, player: int) -> None:
total_locations = world.total_locations[player].value
total_locations = world.worlds[player].options.total_locations.value
num_of_events = total_locations // 25
if total_locations / 25 == num_of_events:
num_of_events -= 1
world_region = world.get_region("Petrichor V", player)
if world.goal[player] == "classic":
if world.worlds[player].options.goal == "classic":
# only setup Pickups when using classic_mode
for i in range(num_of_events):
event_loc = RiskOfRainLocation(player, f"Pickup{(i + 1) * 25}", None, world_region)
@ -254,7 +246,7 @@ def create_events(world: MultiWorld, player: int) -> None:
event_loc.access_rule = \
lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", "Location", player)
world_region.locations.append(event_loc)
elif world.goal[player] == "explore":
elif world.worlds[player].options.goal == "explore":
for n in range(1, 6):
event_region = world.get_region(f"OrderedStage_{n}", player)

View File

@ -148,7 +148,7 @@ class SMWorld(World):
self.remote_items = self.multiworld.remote_items[self.player]
if (len(self.variaRando.randoExec.setup.restrictedLocs) > 0):
self.multiworld.accessibility[self.player] = self.multiworld.accessibility[self.player].from_text("minimal")
self.multiworld.accessibility[self.player].value = Accessibility.option_minimal
logger.warning(f"accessibility forced to 'minimal' for player {self.multiworld.get_player_name(self.player)} because of 'fun' settings")
def create_items(self):

View File

@ -1,18 +1,20 @@
import logging
from typing import Dict, Any, Iterable, Optional, Union, Set
from typing import Dict, Any, Iterable, Optional, Union, Set, List
from BaseClasses import Region, Entrance, Location, Item, Tutorial, CollectionState, ItemClassification, MultiWorld
from Options import PerGameCommonOptions
from worlds.AutoWorld import World, WebWorld
from . import rules, logic, options
from . import rules
from .bundles import get_all_bundles, Bundle
from .items import item_table, create_items, ItemData, Group, items_by_group
from .locations import location_table, create_locations, LocationData
from .logic import StardewLogic, StardewRule, True_, MAX_MONTHS
from .options import stardew_valley_options, StardewOptions, fetch_options
from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, NumberOfLuckBuffs, NumberOfMovementBuffs, \
BackpackProgression, BuildingProgression, ExcludeGingerIsland
from .regions import create_regions
from .rules import set_rules
from worlds.generic.Rules import set_rule
from .strings.goal_names import Goal
from .strings.goal_names import Goal as GoalName
client_version = 0
@ -50,7 +52,6 @@ class StardewValleyWorld(World):
befriend villagers, and uncover dark secrets.
"""
game = "Stardew Valley"
option_definitions = stardew_valley_options
topology_present = False
item_name_to_id = {name: data.code for name, data in item_table.items()}
@ -59,7 +60,8 @@ class StardewValleyWorld(World):
data_version = 3
required_client_version = (0, 4, 0)
options: StardewOptions
options_dataclass = StardewValleyOptions
options: StardewValleyOptions
logic: StardewLogic
web = StardewWebWorld()
@ -72,25 +74,24 @@ class StardewValleyWorld(World):
self.all_progression_items = set()
def generate_early(self):
self.options = fetch_options(self.multiworld, self.player)
self.force_change_options_if_incompatible()
self.logic = StardewLogic(self.player, self.options)
self.modified_bundles = get_all_bundles(self.multiworld.random,
self.logic,
self.options[options.BundleRandomization],
self.options[options.BundlePrice])
self.options.bundle_randomization,
self.options.bundle_price)
def force_change_options_if_incompatible(self):
goal_is_walnut_hunter = self.options[options.Goal] == options.Goal.option_greatest_walnut_hunter
goal_is_perfection = self.options[options.Goal] == options.Goal.option_perfection
goal_is_walnut_hunter = self.options.goal == Goal.option_greatest_walnut_hunter
goal_is_perfection = self.options.goal == Goal.option_perfection
goal_is_island_related = goal_is_walnut_hunter or goal_is_perfection
exclude_ginger_island = self.options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true
exclude_ginger_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
if goal_is_island_related and exclude_ginger_island:
self.options[options.ExcludeGingerIsland] = options.ExcludeGingerIsland.option_false
goal = options.Goal.name_lookup[self.options[options.Goal]]
self.options.exclude_ginger_island.value = ExcludeGingerIsland.option_false
goal_name = self.options.goal.current_key
player_name = self.multiworld.player_name[self.player]
logging.warning(f"Goal '{goal}' requires Ginger Island. Exclude Ginger Island setting forced to 'False' for player {self.player} ({player_name})")
logging.warning(f"Goal '{goal_name}' requires Ginger Island. Exclude Ginger Island setting forced to 'False' for player {self.player} ({player_name})")
def create_regions(self):
def create_region(name: str, exits: Iterable[str]) -> Region:
@ -116,7 +117,7 @@ class StardewValleyWorld(World):
if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK,
Group.FRIENDSHIP_PACK)]
if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled:
if self.options.season_randomization == SeasonRandomization.option_disabled:
items_to_exclude = [item for item in items_to_exclude
if item_table[item.name] not in items_by_group[Group.SEASON]]
@ -134,12 +135,12 @@ class StardewValleyWorld(World):
self.setup_victory()
def precollect_starting_season(self) -> Optional[StardewItem]:
if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive:
if self.options.season_randomization == SeasonRandomization.option_progressive:
return
season_pool = items_by_group[Group.SEASON]
if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled:
if self.options.season_randomization == SeasonRandomization.option_disabled:
for season in season_pool:
self.multiworld.push_precollected(self.create_item(season))
return
@ -148,18 +149,18 @@ class StardewValleyWorld(World):
if item.name in {season.name for season in items_by_group[Group.SEASON]}]:
return
if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_randomized_not_winter:
if self.options.season_randomization == SeasonRandomization.option_randomized_not_winter:
season_pool = [season for season in season_pool if season.name != "Winter"]
starting_season = self.create_item(self.multiworld.random.choice(season_pool))
self.multiworld.push_precollected(starting_season)
def setup_early_items(self):
if (self.options[options.BuildingProgression] ==
options.BuildingProgression.option_progressive_early_shipping_bin):
if (self.options.building_progression ==
BuildingProgression.option_progressive_early_shipping_bin):
self.multiworld.early_items[self.player]["Shipping Bin"] = 1
if self.options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive:
if self.options.backpack_progression == BackpackProgression.option_early_progressive:
self.multiworld.early_items[self.player]["Progressive Backpack"] = 1
def setup_month_events(self):
@ -172,40 +173,40 @@ class StardewValleyWorld(World):
self.create_event_location(month_end, self.logic.received("Month End", i).simplify(), "Month End")
def setup_victory(self):
if self.options[options.Goal] == options.Goal.option_community_center:
self.create_event_location(location_table[Goal.community_center],
if self.options.goal == Goal.option_community_center:
self.create_event_location(location_table[GoalName.community_center],
self.logic.can_complete_community_center().simplify(),
"Victory")
elif self.options[options.Goal] == options.Goal.option_grandpa_evaluation:
self.create_event_location(location_table[Goal.grandpa_evaluation],
elif self.options.goal == Goal.option_grandpa_evaluation:
self.create_event_location(location_table[GoalName.grandpa_evaluation],
self.logic.can_finish_grandpa_evaluation().simplify(),
"Victory")
elif self.options[options.Goal] == options.Goal.option_bottom_of_the_mines:
self.create_event_location(location_table[Goal.bottom_of_the_mines],
elif self.options.goal == Goal.option_bottom_of_the_mines:
self.create_event_location(location_table[GoalName.bottom_of_the_mines],
self.logic.can_mine_to_floor(120).simplify(),
"Victory")
elif self.options[options.Goal] == options.Goal.option_cryptic_note:
self.create_event_location(location_table[Goal.cryptic_note],
elif self.options.goal == Goal.option_cryptic_note:
self.create_event_location(location_table[GoalName.cryptic_note],
self.logic.can_complete_quest("Cryptic Note").simplify(),
"Victory")
elif self.options[options.Goal] == options.Goal.option_master_angler:
self.create_event_location(location_table[Goal.master_angler],
elif self.options.goal == Goal.option_master_angler:
self.create_event_location(location_table[GoalName.master_angler],
self.logic.can_catch_every_fish().simplify(),
"Victory")
elif self.options[options.Goal] == options.Goal.option_complete_collection:
self.create_event_location(location_table[Goal.complete_museum],
elif self.options.goal == Goal.option_complete_collection:
self.create_event_location(location_table[GoalName.complete_museum],
self.logic.can_complete_museum().simplify(),
"Victory")
elif self.options[options.Goal] == options.Goal.option_full_house:
self.create_event_location(location_table[Goal.full_house],
elif self.options.goal == Goal.option_full_house:
self.create_event_location(location_table[GoalName.full_house],
(self.logic.has_children(2) & self.logic.can_reproduce()).simplify(),
"Victory")
elif self.options[options.Goal] == options.Goal.option_greatest_walnut_hunter:
self.create_event_location(location_table[Goal.greatest_walnut_hunter],
elif self.options.goal == Goal.option_greatest_walnut_hunter:
self.create_event_location(location_table[GoalName.greatest_walnut_hunter],
self.logic.has_walnut(130).simplify(),
"Victory")
elif self.options[options.Goal] == options.Goal.option_perfection:
self.create_event_location(location_table[Goal.perfection],
elif self.options.goal == Goal.option_perfection:
self.create_event_location(location_table[GoalName.perfection],
self.logic.has_everything(self.all_progression_items).simplify(),
"Victory")
@ -230,7 +231,7 @@ class StardewValleyWorld(World):
location.place_locked_item(self.create_item(item))
def set_rules(self):
set_rules(self.multiworld, self.player, self.options, self.logic, self.modified_bundles)
set_rules(self)
self.force_first_month_once_all_early_items_are_found()
def force_first_month_once_all_early_items_are_found(self):
@ -276,11 +277,12 @@ class StardewValleyWorld(World):
key, value = self.modified_bundles[bundle_key].to_pair()
modified_bundles[key] = value
excluded_options = [options.BundleRandomization, options.BundlePrice,
options.NumberOfMovementBuffs, options.NumberOfLuckBuffs]
slot_data = dict(self.options.options)
for option in excluded_options:
slot_data.pop(option.internal_name)
excluded_options = [BundleRandomization, BundlePrice, NumberOfMovementBuffs, NumberOfLuckBuffs]
excluded_option_names = [option.internal_name for option in excluded_options]
generic_option_names = [option_name for option_name in PerGameCommonOptions.type_hints]
excluded_option_names.extend(generic_option_names)
included_option_names: List[str] = [option_name for option_name in self.options_dataclass.type_hints if option_name not in excluded_option_names]
slot_data = self.options.as_dict(*included_option_names)
slot_data.update({
"seed": self.multiworld.per_slot_randoms[self.player].randrange(1000000000), # Seed should be max 9 digits
"randomized_entrances": self.randomized_entrances,

View File

@ -152,7 +152,7 @@ class Bundle:
# shuffle_vault_amongst_themselves(random, bundles)
def get_all_bundles(random: Random, logic: StardewLogic, randomization: int, price: int) -> Dict[str, Bundle]:
def get_all_bundles(random: Random, logic: StardewLogic, randomization: BundleRandomization, price: BundlePrice) -> Dict[str, Bundle]:
bundles = {}
for bundle_key in vanilla_bundles:
bundle_value = vanilla_bundles[bundle_key]

View File

@ -7,10 +7,11 @@ from random import Random
from typing import Dict, List, Protocol, Union, Set, Optional
from BaseClasses import Item, ItemClassification
from . import options, data
from . import data
from .data.villagers_data import all_villagers
from .mods.mod_data import ModNames
from .options import StardewOptions
from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Cropsanity, Friendsanity, Museumsanity, \
Fishsanity, BuildingProgression, SkillProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations
from .strings.ap_names.buff_names import Buff
ITEM_CODE_OFFSET = 717000
@ -138,10 +139,9 @@ initialize_groups()
def create_items(item_factory: StardewItemFactory, locations_count: int, items_to_exclude: List[Item],
world_options: StardewOptions,
random: Random) -> List[Item]:
options: StardewValleyOptions, random: Random) -> List[Item]:
items = []
unique_items = create_unique_items(item_factory, world_options, random)
unique_items = create_unique_items(item_factory, options, random)
for item in items_to_exclude:
if item in unique_items:
@ -151,58 +151,58 @@ def create_items(item_factory: StardewItemFactory, locations_count: int, items_t
items += unique_items
logger.debug(f"Created {len(unique_items)} unique items")
unique_filler_items = create_unique_filler_items(item_factory, world_options, random, locations_count - len(items))
unique_filler_items = create_unique_filler_items(item_factory, options, random, locations_count - len(items))
items += unique_filler_items
logger.debug(f"Created {len(unique_filler_items)} unique filler items")
resource_pack_items = fill_with_resource_packs_and_traps(item_factory, world_options, random, items, locations_count)
resource_pack_items = fill_with_resource_packs_and_traps(item_factory, options, random, items, locations_count)
items += resource_pack_items
logger.debug(f"Created {len(resource_pack_items)} resource packs")
return items
def create_unique_items(item_factory: StardewItemFactory, world_options: StardewOptions, random: Random) -> List[Item]:
def create_unique_items(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random) -> List[Item]:
items = []
items.extend(item_factory(item) for item in items_by_group[Group.COMMUNITY_REWARD])
create_backpack_items(item_factory, world_options, items)
create_backpack_items(item_factory, options, items)
create_mine_rewards(item_factory, items, random)
create_elevators(item_factory, world_options, items)
create_tools(item_factory, world_options, items)
create_skills(item_factory, world_options, items)
create_wizard_buildings(item_factory, world_options, items)
create_carpenter_buildings(item_factory, world_options, items)
create_elevators(item_factory, options, items)
create_tools(item_factory, options, items)
create_skills(item_factory, options, items)
create_wizard_buildings(item_factory, options, items)
create_carpenter_buildings(item_factory, options, items)
items.append(item_factory("Beach Bridge"))
items.append(item_factory("Dark Talisman"))
create_tv_channels(item_factory, items)
create_special_quest_rewards(item_factory, items)
create_stardrops(item_factory, world_options, items)
create_museum_items(item_factory, world_options, items)
create_arcade_machine_items(item_factory, world_options, items)
create_stardrops(item_factory, options, items)
create_museum_items(item_factory, options, items)
create_arcade_machine_items(item_factory, options, items)
items.append(item_factory(random.choice(items_by_group[Group.GALAXY_WEAPONS])))
create_player_buffs(item_factory, world_options, items)
create_player_buffs(item_factory, options, items)
create_traveling_merchant_items(item_factory, items)
items.append(item_factory("Return Scepter"))
create_seasons(item_factory, world_options, items)
create_seeds(item_factory, world_options, items)
create_friendsanity_items(item_factory, world_options, items)
create_festival_rewards(item_factory, world_options, items)
create_seasons(item_factory, options, items)
create_seeds(item_factory, options, items)
create_friendsanity_items(item_factory, options, items)
create_festival_rewards(item_factory, options, items)
create_babies(item_factory, items, random)
create_special_order_board_rewards(item_factory, world_options, items)
create_special_order_qi_rewards(item_factory, world_options, items)
create_walnut_purchase_rewards(item_factory, world_options, items)
create_magic_mod_spells(item_factory, world_options, items)
create_special_order_board_rewards(item_factory, options, items)
create_special_order_qi_rewards(item_factory, options, items)
create_walnut_purchase_rewards(item_factory, options, items)
create_magic_mod_spells(item_factory, options, items)
return items
def create_backpack_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
if (world_options[options.BackpackProgression] == options.BackpackProgression.option_progressive or
world_options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive):
def create_backpack_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if (options.backpack_progression == BackpackProgression.option_progressive or
options.backpack_progression == BackpackProgression.option_early_progressive):
items.extend(item_factory(item) for item in ["Progressive Backpack"] * 2)
if ModNames.big_backpack in world_options[options.Mods]:
if ModNames.big_backpack in options.mods:
items.append(item_factory("Progressive Backpack"))
@ -220,46 +220,46 @@ def create_mine_rewards(item_factory: StardewItemFactory, items: List[Item], ran
items.append(item_factory("Skull Key"))
def create_elevators(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
if world_options[options.ElevatorProgression] == options.ElevatorProgression.option_vanilla:
def create_elevators(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.elevator_progression == ElevatorProgression.option_vanilla:
return
items.extend([item_factory(item) for item in ["Progressive Mine Elevator"] * 24])
if ModNames.deepwoods in world_options[options.Mods]:
if ModNames.deepwoods in options.mods:
items.extend([item_factory(item) for item in ["Progressive Woods Obelisk Sigils"] * 10])
if ModNames.skull_cavern_elevator in world_options[options.Mods]:
if ModNames.skull_cavern_elevator in options.mods:
items.extend([item_factory(item) for item in ["Progressive Skull Cavern Elevator"] * 8])
def create_tools(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
if world_options[options.ToolProgression] == options.ToolProgression.option_progressive:
def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.tool_progression == ToolProgression.option_progressive:
items.extend(item_factory(item) for item in items_by_group[Group.PROGRESSIVE_TOOLS] * 4)
items.append(item_factory("Golden Scythe"))
def create_skills(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
if world_options[options.SkillProgression] == options.SkillProgression.option_progressive:
def create_skills(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.skill_progression == SkillProgression.option_progressive:
for item in items_by_group[Group.SKILL_LEVEL_UP]:
if item.mod_name not in world_options[options.Mods] and item.mod_name is not None:
if item.mod_name not in options.mods and item.mod_name is not None:
continue
items.extend(item_factory(item) for item in [item.name] * 10)
def create_wizard_buildings(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
def create_wizard_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
items.append(item_factory("Earth Obelisk"))
items.append(item_factory("Water Obelisk"))
items.append(item_factory("Desert Obelisk"))
items.append(item_factory("Junimo Hut"))
items.append(item_factory("Gold Clock"))
if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_false:
if options.exclude_ginger_island == ExcludeGingerIsland.option_false:
items.append(item_factory("Island Obelisk"))
if ModNames.deepwoods in world_options[options.Mods]:
if ModNames.deepwoods in options.mods:
items.append(item_factory("Woods Obelisk"))
def create_carpenter_buildings(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
if world_options[options.BuildingProgression] in {options.BuildingProgression.option_progressive,
options.BuildingProgression.option_progressive_early_shipping_bin}:
def create_carpenter_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.building_progression in {BuildingProgression.option_progressive,
BuildingProgression.option_progressive_early_shipping_bin}:
items.append(item_factory("Progressive Coop"))
items.append(item_factory("Progressive Coop"))
items.append(item_factory("Progressive Coop"))
@ -278,7 +278,7 @@ def create_carpenter_buildings(item_factory: StardewItemFactory, world_options:
items.append(item_factory("Progressive House"))
items.append(item_factory("Progressive House"))
items.append(item_factory("Progressive House"))
if ModNames.tractor in world_options[options.Mods]:
if ModNames.tractor in options.mods:
items.append(item_factory("Tractor Garage"))
@ -290,17 +290,17 @@ def create_special_quest_rewards(item_factory: StardewItemFactory, items: List[I
items.append(item_factory("Iridium Snake Milk"))
def create_stardrops(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
def create_stardrops(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
items.append(item_factory("Stardrop")) # The Mines level 100
items.append(item_factory("Stardrop")) # Old Master Cannoli
if world_options[options.Fishsanity] != options.Fishsanity.option_none:
if options.fishsanity != Fishsanity.option_none:
items.append(item_factory("Stardrop")) #Master Angler Stardrop
if ModNames.deepwoods in world_options[options.Mods]:
if ModNames.deepwoods in options.mods:
items.append(item_factory("Stardrop")) # Petting the Unicorn
def create_museum_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
if world_options[options.Museumsanity] == options.Museumsanity.option_none:
def create_museum_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.museumsanity == Museumsanity.option_none:
return
items.extend(item_factory(item) for item in ["Magic Rock Candy"] * 5)
items.extend(item_factory(item) for item in ["Ancient Seeds"] * 5)
@ -311,17 +311,17 @@ def create_museum_items(item_factory: StardewItemFactory, world_options: Stardew
items.append(item_factory("Dwarvish Translation Guide"))
def create_friendsanity_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
if world_options[options.Friendsanity] == options.Friendsanity.option_none:
def create_friendsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.friendsanity == Friendsanity.option_none:
return
exclude_non_bachelors = world_options[options.Friendsanity] == options.Friendsanity.option_bachelors
exclude_locked_villagers = world_options[options.Friendsanity] == options.Friendsanity.option_starting_npcs or \
world_options[options.Friendsanity] == options.Friendsanity.option_bachelors
include_post_marriage_hearts = world_options[options.Friendsanity] == options.Friendsanity.option_all_with_marriage
exclude_ginger_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true
heart_size = world_options[options.FriendsanityHeartSize]
exclude_non_bachelors = options.friendsanity == Friendsanity.option_bachelors
exclude_locked_villagers = options.friendsanity == Friendsanity.option_starting_npcs or \
options.friendsanity == Friendsanity.option_bachelors
include_post_marriage_hearts = options.friendsanity == Friendsanity.option_all_with_marriage
exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true
heart_size = options.friendsanity_heart_size
for villager in all_villagers:
if villager.mod_name not in world_options[options.Mods] and villager.mod_name is not None:
if villager.mod_name not in options.mods and villager.mod_name is not None:
continue
if not villager.available and exclude_locked_villagers:
continue
@ -350,8 +350,8 @@ def create_babies(item_factory: StardewItemFactory, items: List[Item], random: R
items.append(item_factory(chosen_baby))
def create_arcade_machine_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling:
def create_arcade_machine_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.arcade_machine_locations == ArcadeMachineLocations.option_full_shuffling:
items.append(item_factory("JotPK: Progressive Boots"))
items.append(item_factory("JotPK: Progressive Boots"))
items.append(item_factory("JotPK: Progressive Gun"))
@ -367,11 +367,9 @@ def create_arcade_machine_items(item_factory: StardewItemFactory, world_options:
items.extend(item_factory(item) for item in ["Junimo Kart: Extra Life"] * 8)
def create_player_buffs(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]):
number_of_movement_buffs: int = world_options[options.NumberOfMovementBuffs]
number_of_luck_buffs: int = world_options[options.NumberOfLuckBuffs]
items.extend(item_factory(item) for item in [Buff.movement] * number_of_movement_buffs)
items.extend(item_factory(item) for item in [Buff.luck] * number_of_luck_buffs)
def create_player_buffs(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
items.extend(item_factory(item) for item in [Buff.movement] * options.number_of_movement_buffs.value)
items.extend(item_factory(item) for item in [Buff.luck] * options.number_of_luck_buffs.value)
def create_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]):
@ -380,36 +378,36 @@ def create_traveling_merchant_items(item_factory: StardewItemFactory, items: Lis
*(item_factory(item) for item in ["Traveling Merchant Discount"] * 8)])
def create_seasons(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
if world_options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled:
def create_seasons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.season_randomization == SeasonRandomization.option_disabled:
return
if world_options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive:
if options.season_randomization == SeasonRandomization.option_progressive:
items.extend([item_factory(item) for item in ["Progressive Season"] * 3])
return
items.extend([item_factory(item) for item in items_by_group[Group.SEASON]])
def create_seeds(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
if world_options[options.Cropsanity] == options.Cropsanity.option_disabled:
def create_seeds(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.cropsanity == Cropsanity.option_disabled:
return
include_ginger_island = world_options[options.ExcludeGingerIsland] != options.ExcludeGingerIsland.option_true
include_ginger_island = options.exclude_ginger_island != ExcludeGingerIsland.option_true
seed_items = [item_factory(item) for item in items_by_group[Group.CROPSANITY] if include_ginger_island or Group.GINGER_ISLAND not in item.groups]
items.extend(seed_items)
def create_festival_rewards(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
if world_options[options.FestivalLocations] == options.FestivalLocations.option_disabled:
def create_festival_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.festival_locations == FestivalLocations.option_disabled:
return
items.extend([*[item_factory(item) for item in items_by_group[Group.FESTIVAL] if item.classification != ItemClassification.filler],
item_factory("Stardrop")])
def create_walnut_purchase_rewards(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true:
def create_walnut_purchase_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return
items.extend([item_factory("Boat Repair"),
@ -420,16 +418,16 @@ def create_walnut_purchase_rewards(item_factory: StardewItemFactory, world_optio
def create_special_order_board_rewards(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled:
def create_special_order_board_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.special_order_locations == SpecialOrderLocations.option_disabled:
return
items.extend([item_factory(item) for item in items_by_group[Group.SPECIAL_ORDER_BOARD]])
def create_special_order_qi_rewards(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
if (world_options[options.SpecialOrderLocations] != options.SpecialOrderLocations.option_board_qi or
world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true):
def create_special_order_qi_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if (options.special_order_locations != SpecialOrderLocations.option_board_qi or
options.exclude_ginger_island == ExcludeGingerIsland.option_true):
return
qi_gem_rewards = ["100 Qi Gems", "10 Qi Gems", "40 Qi Gems", "25 Qi Gems", "25 Qi Gems",
"40 Qi Gems", "20 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems"]
@ -441,35 +439,35 @@ def create_tv_channels(item_factory: StardewItemFactory, items: List[Item]):
items.extend([item_factory(item) for item in items_by_group[Group.TV_CHANNEL]])
def create_filler_festival_rewards(item_factory: StardewItemFactory, world_options: StardewOptions) -> List[Item]:
if world_options[options.FestivalLocations] == options.FestivalLocations.option_disabled:
def create_filler_festival_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions) -> List[Item]:
if options.festival_locations == FestivalLocations.option_disabled:
return []
return [item_factory(item) for item in items_by_group[Group.FESTIVAL] if
item.classification == ItemClassification.filler]
def create_magic_mod_spells(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
if ModNames.magic not in world_options[options.Mods]:
def create_magic_mod_spells(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if ModNames.magic not in options.mods:
return []
items.extend([item_factory(item) for item in items_by_group[Group.MAGIC_SPELL]])
def create_unique_filler_items(item_factory: StardewItemFactory, world_options: options.StardewOptions, random: Random,
def create_unique_filler_items(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random,
available_item_slots: int) -> List[Item]:
items = []
items.extend(create_filler_festival_rewards(item_factory, world_options))
items.extend(create_filler_festival_rewards(item_factory, options))
if len(items) > available_item_slots:
items = random.sample(items, available_item_slots)
return items
def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, world_options: options.StardewOptions, random: Random,
def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random,
items_already_added: List[Item],
number_locations: int) -> List[Item]:
include_traps = world_options[options.TrapItems] != options.TrapItems.option_no_traps
include_traps = options.trap_items != TrapItems.option_no_traps
all_filler_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK]]
all_filler_packs.extend(items_by_group[Group.TRASH])
if include_traps:
@ -479,15 +477,15 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, world_o
if pack.name not in items_already_added_names]
trap_items = [pack for pack in items_by_group[Group.TRAP]
if pack.name not in items_already_added_names and
(pack.mod_name is None or pack.mod_name in world_options[options.Mods])]
(pack.mod_name is None or pack.mod_name in options.mods)]
priority_filler_items = []
priority_filler_items.extend(useful_resource_packs)
if include_traps:
priority_filler_items.extend(trap_items)
all_filler_packs = remove_excluded_packs(all_filler_packs, world_options)
priority_filler_items = remove_excluded_packs(priority_filler_items, world_options)
all_filler_packs = remove_excluded_packs(all_filler_packs, options)
priority_filler_items = remove_excluded_packs(priority_filler_items, options)
number_priority_items = len(priority_filler_items)
required_resource_pack = number_locations - len(items_already_added)
@ -521,8 +519,8 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, world_o
return items
def remove_excluded_packs(packs, world_options):
def remove_excluded_packs(packs, options: StardewValleyOptions):
included_packs = [pack for pack in packs if Group.DEPRECATED not in pack.groups]
if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true:
if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
included_packs = [pack for pack in included_packs if Group.GINGER_ISLAND not in pack.groups]
return included_packs

View File

@ -4,10 +4,12 @@ from dataclasses import dataclass
from random import Random
from typing import Optional, Dict, Protocol, List, FrozenSet
from . import options, data
from . import data
from .options import StardewValleyOptions
from .data.fish_data import legendary_fish, special_fish, all_fish
from .data.museum_data import all_museum_items
from .data.villagers_data import all_villagers
from .options import ExcludeGingerIsland, Friendsanity, ArcadeMachineLocations, SpecialOrderLocations, Cropsanity, Fishsanity, Museumsanity, FestivalLocations, SkillProgression, BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression
from .strings.goal_names import Goal
from .strings.region_names import Region
@ -133,12 +135,12 @@ def initialize_groups():
initialize_groups()
def extend_cropsanity_locations(randomized_locations: List[LocationData], world_options):
if world_options[options.Cropsanity] == options.Cropsanity.option_disabled:
def extend_cropsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
if options.cropsanity == Cropsanity.option_disabled:
return
cropsanity_locations = locations_by_tag[LocationTags.CROPSANITY]
cropsanity_locations = filter_ginger_island(world_options, cropsanity_locations)
cropsanity_locations = filter_ginger_island(options, cropsanity_locations)
randomized_locations.extend(cropsanity_locations)
@ -157,56 +159,56 @@ def extend_help_wanted_quests(randomized_locations: List[LocationData], desired_
randomized_locations.append(location_table[f"Help Wanted: Gathering {batch + 1}"])
def extend_fishsanity_locations(randomized_locations: List[LocationData], world_options, random: Random):
def extend_fishsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random):
prefix = "Fishsanity: "
if world_options[options.Fishsanity] == options.Fishsanity.option_none:
if options.fishsanity == Fishsanity.option_none:
return
elif world_options[options.Fishsanity] == options.Fishsanity.option_legendaries:
elif options.fishsanity == Fishsanity.option_legendaries:
randomized_locations.extend(location_table[f"{prefix}{legendary.name}"] for legendary in legendary_fish)
elif world_options[options.Fishsanity] == options.Fishsanity.option_special:
elif options.fishsanity == Fishsanity.option_special:
randomized_locations.extend(location_table[f"{prefix}{special.name}"] for special in special_fish)
elif world_options[options.Fishsanity] == options.Fishsanity.option_randomized:
elif options.fishsanity == Fishsanity.option_randomized:
fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if random.random() < 0.4]
randomized_locations.extend(filter_ginger_island(world_options, fish_locations))
elif world_options[options.Fishsanity] == options.Fishsanity.option_all:
randomized_locations.extend(filter_ginger_island(options, fish_locations))
elif options.fishsanity == Fishsanity.option_all:
fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish]
randomized_locations.extend(filter_ginger_island(world_options, fish_locations))
elif world_options[options.Fishsanity] == options.Fishsanity.option_exclude_legendaries:
randomized_locations.extend(filter_ginger_island(options, fish_locations))
elif options.fishsanity == Fishsanity.option_exclude_legendaries:
fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if fish not in legendary_fish]
randomized_locations.extend(filter_ginger_island(world_options, fish_locations))
elif world_options[options.Fishsanity] == options.Fishsanity.option_exclude_hard_fish:
randomized_locations.extend(filter_ginger_island(options, fish_locations))
elif options.fishsanity == Fishsanity.option_exclude_hard_fish:
fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if fish.difficulty < 80]
randomized_locations.extend(filter_ginger_island(world_options, fish_locations))
elif world_options[options.Fishsanity] == options.Fishsanity.option_only_easy_fish:
randomized_locations.extend(filter_ginger_island(options, fish_locations))
elif options.fishsanity == Fishsanity.option_only_easy_fish:
fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if fish.difficulty < 50]
randomized_locations.extend(filter_ginger_island(world_options, fish_locations))
randomized_locations.extend(filter_ginger_island(options, fish_locations))
def extend_museumsanity_locations(randomized_locations: List[LocationData], museumsanity: int, random: Random):
def extend_museumsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random):
prefix = "Museumsanity: "
if museumsanity == options.Museumsanity.option_none:
if options.museumsanity == Museumsanity.option_none:
return
elif museumsanity == options.Museumsanity.option_milestones:
elif options.museumsanity == Museumsanity.option_milestones:
randomized_locations.extend(locations_by_tag[LocationTags.MUSEUM_MILESTONES])
elif museumsanity == options.Museumsanity.option_randomized:
elif options.museumsanity == Museumsanity.option_randomized:
randomized_locations.extend(location_table[f"{prefix}{museum_item.name}"]
for museum_item in all_museum_items if random.random() < 0.4)
elif museumsanity == options.Museumsanity.option_all:
elif options.museumsanity == Museumsanity.option_all:
randomized_locations.extend(location_table[f"{prefix}{museum_item.name}"] for museum_item in all_museum_items)
def extend_friendsanity_locations(randomized_locations: List[LocationData], world_options: options.StardewOptions):
if world_options[options.Friendsanity] == options.Friendsanity.option_none:
def extend_friendsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
if options.friendsanity == Friendsanity.option_none:
return
exclude_leo = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true
exclude_non_bachelors = world_options[options.Friendsanity] == options.Friendsanity.option_bachelors
exclude_locked_villagers = world_options[options.Friendsanity] == options.Friendsanity.option_starting_npcs or \
world_options[options.Friendsanity] == options.Friendsanity.option_bachelors
include_post_marriage_hearts = world_options[options.Friendsanity] == options.Friendsanity.option_all_with_marriage
heart_size = world_options[options.FriendsanityHeartSize]
exclude_leo = options.exclude_ginger_island == ExcludeGingerIsland.option_true
exclude_non_bachelors = options.friendsanity == Friendsanity.option_bachelors
exclude_locked_villagers = options.friendsanity == Friendsanity.option_starting_npcs or \
options.friendsanity == Friendsanity.option_bachelors
include_post_marriage_hearts = options.friendsanity == Friendsanity.option_all_with_marriage
heart_size = options.friendsanity_heart_size
for villager in all_villagers:
if villager.mod_name not in world_options[options.Mods] and villager.mod_name is not None:
if villager.mod_name not in options.mods and villager.mod_name is not None:
continue
if not villager.available and exclude_locked_villagers:
continue
@ -228,38 +230,38 @@ def extend_friendsanity_locations(randomized_locations: List[LocationData], worl
randomized_locations.append(location_table[f"Friendsanity: Pet {heart} <3"])
def extend_festival_locations(randomized_locations: List[LocationData], festival_option: int):
if festival_option == options.FestivalLocations.option_disabled:
def extend_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
if options.festival_locations == FestivalLocations.option_disabled:
return
festival_locations = locations_by_tag[LocationTags.FESTIVAL]
randomized_locations.extend(festival_locations)
extend_hard_festival_locations(randomized_locations, festival_option)
extend_hard_festival_locations(randomized_locations, options)
def extend_hard_festival_locations(randomized_locations, festival_option: int):
if festival_option != options.FestivalLocations.option_hard:
def extend_hard_festival_locations(randomized_locations, options: StardewValleyOptions):
if options.festival_locations != FestivalLocations.option_hard:
return
hard_festival_locations = locations_by_tag[LocationTags.FESTIVAL_HARD]
randomized_locations.extend(hard_festival_locations)
def extend_special_order_locations(randomized_locations: List[LocationData], world_options):
if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled:
def extend_special_order_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
if options.special_order_locations == SpecialOrderLocations.option_disabled:
return
include_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_false
board_locations = filter_disabled_locations(world_options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD])
include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false
board_locations = filter_disabled_locations(options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD])
randomized_locations.extend(board_locations)
if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_board_qi and include_island:
include_arcade = world_options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_disabled
if options.special_order_locations == SpecialOrderLocations.option_board_qi and include_island:
include_arcade = options.arcade_machine_locations != ArcadeMachineLocations.option_disabled
qi_orders = [location for location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI] if include_arcade or LocationTags.JUNIMO_KART not in location.tags]
randomized_locations.extend(qi_orders)
def extend_walnut_purchase_locations(randomized_locations: List[LocationData], world_options):
if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true:
def extend_walnut_purchase_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return
randomized_locations.append(location_table["Repair Ticket Machine"])
randomized_locations.append(location_table["Repair Boat Hull"])
@ -269,82 +271,82 @@ def extend_walnut_purchase_locations(randomized_locations: List[LocationData], w
randomized_locations.extend(locations_by_tag[LocationTags.WALNUT_PURCHASE])
def extend_mandatory_locations(randomized_locations: List[LocationData], world_options):
def extend_mandatory_locations(randomized_locations: List[LocationData], options):
mandatory_locations = [location for location in locations_by_tag[LocationTags.MANDATORY]]
filtered_mandatory_locations = filter_disabled_locations(world_options, mandatory_locations)
filtered_mandatory_locations = filter_disabled_locations(options, mandatory_locations)
randomized_locations.extend(filtered_mandatory_locations)
def extend_backpack_locations(randomized_locations: List[LocationData], world_options):
if world_options[options.BackpackProgression] == options.BackpackProgression.option_vanilla:
def extend_backpack_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
if options.backpack_progression == BackpackProgression.option_vanilla:
return
backpack_locations = [location for location in locations_by_tag[LocationTags.BACKPACK]]
filtered_backpack_locations = filter_modded_locations(world_options, backpack_locations)
filtered_backpack_locations = filter_modded_locations(options, backpack_locations)
randomized_locations.extend(filtered_backpack_locations)
def extend_elevator_locations(randomized_locations: List[LocationData], world_options):
if world_options[options.ElevatorProgression] == options.ElevatorProgression.option_vanilla:
def extend_elevator_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
if options.elevator_progression == ElevatorProgression.option_vanilla:
return
elevator_locations = [location for location in locations_by_tag[LocationTags.ELEVATOR]]
filtered_elevator_locations = filter_modded_locations(world_options, elevator_locations)
filtered_elevator_locations = filter_modded_locations(options, elevator_locations)
randomized_locations.extend(filtered_elevator_locations)
def create_locations(location_collector: StardewLocationCollector,
world_options: options.StardewOptions,
options: StardewValleyOptions,
random: Random):
randomized_locations = []
extend_mandatory_locations(randomized_locations, world_options)
extend_backpack_locations(randomized_locations, world_options)
extend_mandatory_locations(randomized_locations, options)
extend_backpack_locations(randomized_locations, options)
if not world_options[options.ToolProgression] == options.ToolProgression.option_vanilla:
if not options.tool_progression == ToolProgression.option_vanilla:
randomized_locations.extend(locations_by_tag[LocationTags.TOOL_UPGRADE])
extend_elevator_locations(randomized_locations, world_options)
extend_elevator_locations(randomized_locations, options)
if not world_options[options.SkillProgression] == options.SkillProgression.option_vanilla:
if not options.skill_progression == SkillProgression.option_vanilla:
for location in locations_by_tag[LocationTags.SKILL_LEVEL]:
if location.mod_name is None or location.mod_name in world_options[options.Mods]:
if location.mod_name is None or location.mod_name in options.mods:
randomized_locations.append(location_table[location.name])
if not world_options[options.BuildingProgression] == options.BuildingProgression.option_vanilla:
if not options.building_progression == BuildingProgression.option_vanilla:
for location in locations_by_tag[LocationTags.BUILDING_BLUEPRINT]:
if location.mod_name is None or location.mod_name in world_options[options.Mods]:
if location.mod_name is None or location.mod_name in options.mods:
randomized_locations.append(location_table[location.name])
if world_options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_disabled:
if options.arcade_machine_locations != ArcadeMachineLocations.option_disabled:
randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE_VICTORY])
if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling:
if options.arcade_machine_locations == ArcadeMachineLocations.option_full_shuffling:
randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE])
extend_cropsanity_locations(randomized_locations, world_options)
extend_help_wanted_quests(randomized_locations, world_options[options.HelpWantedLocations])
extend_fishsanity_locations(randomized_locations, world_options, random)
extend_museumsanity_locations(randomized_locations, world_options[options.Museumsanity], random)
extend_friendsanity_locations(randomized_locations, world_options)
extend_cropsanity_locations(randomized_locations, options)
extend_help_wanted_quests(randomized_locations, options.help_wanted_locations.value)
extend_fishsanity_locations(randomized_locations, options, random)
extend_museumsanity_locations(randomized_locations, options, random)
extend_friendsanity_locations(randomized_locations, options)
extend_festival_locations(randomized_locations, world_options[options.FestivalLocations])
extend_special_order_locations(randomized_locations, world_options)
extend_walnut_purchase_locations(randomized_locations, world_options)
extend_festival_locations(randomized_locations, options)
extend_special_order_locations(randomized_locations, options)
extend_walnut_purchase_locations(randomized_locations, options)
for location_data in randomized_locations:
location_collector(location_data.name, location_data.code, location_data.region)
def filter_ginger_island(world_options: options.StardewOptions, locations: List[LocationData]) -> List[LocationData]:
include_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_false
def filter_ginger_island(options: StardewValleyOptions, locations: List[LocationData]) -> List[LocationData]:
include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false
return [location for location in locations if include_island or LocationTags.GINGER_ISLAND not in location.tags]
def filter_modded_locations(world_options: options.StardewOptions, locations: List[LocationData]) -> List[LocationData]:
current_mod_names = world_options[options.Mods]
def filter_modded_locations(options: StardewValleyOptions, locations: List[LocationData]) -> List[LocationData]:
current_mod_names = options.mods
return [location for location in locations if location.mod_name is None or location.mod_name in current_mod_names]
def filter_disabled_locations(world_options: options.StardewOptions, locations: List[LocationData]) -> List[LocationData]:
locations_first_pass = filter_ginger_island(world_options, locations)
locations_second_pass = filter_modded_locations(world_options, locations_first_pass)
def filter_disabled_locations(options: StardewValleyOptions, locations: List[LocationData]) -> List[LocationData]:
locations_first_pass = filter_ginger_island(options, locations)
locations_second_pass = filter_modded_locations(options, locations_first_pass)
return locations_second_pass

View File

@ -4,7 +4,6 @@ import math
from dataclasses import dataclass, field
from typing import Dict, Union, Optional, Iterable, Sized, List, Set
from . import options
from .data import all_fish, FishItem, all_purchasable_seeds, SeedItem, all_crops, CropItem
from .data.bundle_data import BundleItem
from .data.crops_data import crops_by_name
@ -20,7 +19,8 @@ from .mods.logic.special_orders import get_modded_special_orders_rules
from .mods.logic.skullcavernelevator import has_skull_cavern_elevator_to_floor
from .mods.mod_data import ModNames
from .mods.logic import magic, skills
from .options import StardewOptions
from .options import Museumsanity, SeasonRandomization, StardewValleyOptions, BuildingProgression, SkillProgression, ToolProgression, Friendsanity, Cropsanity, \
ExcludeGingerIsland, ElevatorProgression, ArcadeMachineLocations, FestivalLocations, SpecialOrderLocations
from .regions import vanilla_regions
from .stardew_rule import False_, Reach, Or, True_, Received, Count, And, Has, TotalReceived, StardewRule
from .strings.animal_names import Animal, coop_animals, barn_animals
@ -81,10 +81,11 @@ tool_upgrade_prices = {
fishing_regions = [Region.beach, Region.town, Region.forest, Region.mountain, Region.island_south, Region.island_west]
@dataclass(frozen=True, repr=False)
class StardewLogic:
player: int
options: StardewOptions
options: StardewValleyOptions
item_rules: Dict[str, StardewRule] = field(default_factory=dict)
sapling_rules: Dict[str, StardewRule] = field(default_factory=dict)
@ -398,7 +399,7 @@ class StardewLogic:
Building.cellar: self.can_spend_money_at(Region.carpenter, 100000) & self.has_house(2),
})
self.building_rules.update(get_modded_building_rules(self, self.options[options.Mods]))
self.building_rules.update(get_modded_building_rules(self, self.options.mods))
self.quest_rules.update({
Quest.introductions: self.can_reach_region(Region.town),
@ -455,7 +456,7 @@ class StardewLogic:
self.can_meet(NPC.wizard) & self.can_meet(NPC.willy),
})
self.quest_rules.update(get_modded_quest_rules(self, self.options[options.Mods]))
self.quest_rules.update(get_modded_quest_rules(self, self.options.mods))
self.festival_rules.update({
FestivalCheck.egg_hunt: self.has_season(Season.spring) & self.can_reach_region(Region.town) & self.can_win_egg_hunt(),
@ -539,7 +540,7 @@ class StardewLogic:
self.can_spend_money(80000), # I need this extra rule because money rules aren't additive...
})
self.special_order_rules.update(get_modded_special_orders_rules(self, self.options[options.Mods]))
self.special_order_rules.update(get_modded_special_orders_rules(self, self.options.mods))
def has(self, items: Union[str, (Iterable[str], Sized)], count: Optional[int] = None) -> StardewRule:
if isinstance(items, str):
@ -596,7 +597,7 @@ class StardewLogic:
return self.has_lived_months(min(8, amount // MONEY_PER_MONTH))
def can_spend_money(self, amount: int) -> StardewRule:
if self.options[options.StartingMoney] == -1:
if self.options.starting_money == -1:
return True_()
return self.has_lived_months(min(8, amount // (MONEY_PER_MONTH // 5)))
@ -607,7 +608,7 @@ class StardewLogic:
if material == ToolMaterial.basic or tool == Tool.scythe:
return True_()
if self.options[options.ToolProgression] == options.ToolProgression.option_progressive:
if self.options.tool_progression == ToolProgression.option_progressive:
return self.received(f"Progressive {tool}", count=tool_materials[material])
return self.has(f"{material} Bar") & self.can_spend_money(tool_upgrade_prices[material])
@ -644,7 +645,7 @@ class StardewLogic:
if level <= 0:
return True_()
if self.options[options.SkillProgression] == options.SkillProgression.option_progressive:
if self.options.skill_progression == SkillProgression.option_progressive:
return self.received(f"{skill} Level", count=level)
return self.can_earn_skill_level(skill, level)
@ -656,7 +657,7 @@ class StardewLogic:
if level <= 0:
return True_()
if self.options[options.SkillProgression] == options.SkillProgression.option_progressive:
if self.options.skill_progression == SkillProgression.option_progressive:
skills_items = ["Farming Level", "Mining Level", "Foraging Level",
"Fishing Level", "Combat Level"]
if allow_modded_skills:
@ -672,7 +673,7 @@ class StardewLogic:
def has_building(self, building: str) -> StardewRule:
carpenter_rule = self.can_reach_region(Region.carpenter)
if not self.options[options.BuildingProgression] == options.BuildingProgression.option_vanilla:
if not self.options.building_progression == BuildingProgression.option_vanilla:
count = 1
if building in [Building.coop, Building.barn, Building.shed]:
building = f"Progressive {building}"
@ -693,7 +694,7 @@ class StardewLogic:
if upgrade_level > 3:
return False_()
if not self.options[options.BuildingProgression] == options.BuildingProgression.option_vanilla:
if not self.options.building_progression == BuildingProgression.option_vanilla:
return self.received(f"Progressive House", upgrade_level) & self.can_reach_region(Region.carpenter)
if upgrade_level == 1:
@ -734,7 +735,7 @@ class StardewLogic:
return tool_rule & enemy_rule
def can_get_fishing_xp(self) -> StardewRule:
if self.options[options.SkillProgression] == options.SkillProgression.option_progressive:
if self.options.skill_progression == SkillProgression.option_progressive:
return self.can_fish() | self.can_crab_pot()
return self.can_fish()
@ -746,7 +747,7 @@ class StardewLogic:
skill_rule = self.has_skill_level(Skill.fishing, skill_required)
region_rule = self.can_reach_any_region(fishing_regions)
number_fishing_rod_required = 1 if difficulty < 50 else 2
if self.options[options.ToolProgression] == options.ToolProgression.option_progressive:
if self.options.tool_progression == ToolProgression.option_progressive:
return self.received("Progressive Fishing Rod", number_fishing_rod_required) & skill_rule & region_rule
return skill_rule & region_rule
@ -763,7 +764,7 @@ class StardewLogic:
return self.has_max_fishing_rod() & skill_rule
def can_buy_seed(self, seed: SeedItem) -> StardewRule:
if self.options[options.Cropsanity] == options.Cropsanity.option_disabled:
if self.options.cropsanity == Cropsanity.option_disabled:
item_rule = True_()
else:
item_rule = self.received(seed.name)
@ -781,7 +782,7 @@ class StardewLogic:
Fruit.peach: 6000,
Fruit.pomegranate: 6000, Fruit.banana: 0, Fruit.mango: 0}
received_sapling = self.received(f"{fruit} Sapling")
if self.options[options.Cropsanity] == options.Cropsanity.option_disabled:
if self.options.cropsanity == Cropsanity.option_disabled:
allowed_buy_sapling = True_()
else:
allowed_buy_sapling = received_sapling
@ -824,14 +825,14 @@ class StardewLogic:
def can_catch_every_fish(self) -> StardewRule:
rules = [self.has_skill_level(Skill.fishing, 10), self.has_max_fishing_rod()]
for fish in all_fish:
if self.options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true and \
if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true and \
fish in island_fish:
continue
rules.append(self.can_catch_fish(fish))
return And(rules)
def has_max_fishing_rod(self) -> StardewRule:
if self.options[options.ToolProgression] == options.ToolProgression.option_progressive:
if self.options.tool_progression == ToolProgression.option_progressive:
return self.received(APTool.fishing_rod, 4)
return self.can_get_fishing_xp()
@ -875,7 +876,7 @@ class StardewLogic:
def can_crab_pot(self, region: str = Generic.any) -> StardewRule:
crab_pot_rule = self.has(Craftable.bait)
if self.options[options.SkillProgression] == options.SkillProgression.option_progressive:
if self.options.skill_progression == SkillProgression.option_progressive:
crab_pot_rule = crab_pot_rule & self.has(Machine.crab_pot)
else:
crab_pot_rule = crab_pot_rule & self.can_get_fishing_xp()
@ -926,9 +927,7 @@ class StardewLogic:
return region_rule & ((tool_rule & foraging_rule) | magic_rule)
def has_max_buffs(self) -> StardewRule:
number_of_movement_buffs: int = self.options[options.NumberOfMovementBuffs]
number_of_luck_buffs: int = self.options[options.NumberOfLuckBuffs]
return self.received(Buff.movement, number_of_movement_buffs) & self.received(Buff.luck, number_of_luck_buffs)
return self.received(Buff.movement, self.options.number_of_movement_buffs.value) & self.received(Buff.luck, self.options.number_of_luck_buffs.value)
def get_weapon_rule_for_floor_tier(self, tier: int):
if tier >= 4:
@ -946,9 +945,9 @@ class StardewLogic:
rules = []
weapon_rule = self.get_weapon_rule_for_floor_tier(tier)
rules.append(weapon_rule)
if self.options[options.ToolProgression] == options.ToolProgression.option_progressive:
if self.options.tool_progression == ToolProgression.option_progressive:
rules.append(self.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier]))
if self.options[options.SkillProgression] == options.SkillProgression.option_progressive:
if self.options.skill_progression == SkillProgression.option_progressive:
combat_tier = min(10, max(0, tier * 2))
rules.append(self.has_skill_level(Skill.combat, combat_tier))
return And(rules)
@ -958,15 +957,15 @@ class StardewLogic:
rules = []
weapon_rule = self.get_weapon_rule_for_floor_tier(tier)
rules.append(weapon_rule)
if self.options[options.ToolProgression] == options.ToolProgression.option_progressive:
if self.options.tool_progression == ToolProgression.option_progressive:
rules.append(self.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier]))
if self.options[options.SkillProgression] == options.SkillProgression.option_progressive:
if self.options.skill_progression == SkillProgression.option_progressive:
combat_tier = min(10, max(0, tier * 2))
rules.append(self.has_skill_level(Skill.combat, combat_tier))
return And(rules)
def has_mine_elevator_to_floor(self, floor: int) -> StardewRule:
if self.options[options.ElevatorProgression] != options.ElevatorProgression.option_vanilla:
if self.options.elevator_progression != ElevatorProgression.option_vanilla:
return self.received("Progressive Mine Elevator", count=int(floor / 5))
return True_()
@ -984,9 +983,9 @@ class StardewLogic:
weapon_rule = self.has_great_weapon()
rules.append(weapon_rule)
rules.append(self.can_cook())
if self.options[options.ToolProgression] == options.ToolProgression.option_progressive:
if self.options.tool_progression == ToolProgression.option_progressive:
rules.append(self.received("Progressive Pickaxe", min(4, max(0, tier + 2))))
if self.options[options.SkillProgression] == options.SkillProgression.option_progressive:
if self.options.skill_progression == SkillProgression.option_progressive:
skill_tier = min(10, max(0, tier * 2 + 6))
rules.extend({self.has_skill_level(Skill.combat, skill_tier),
self.has_skill_level(Skill.mining, skill_tier)})
@ -1005,20 +1004,20 @@ class StardewLogic:
self.can_progress_easily_in_the_skull_cavern_from_floor(previous_previous_elevator))) & has_mine_elevator
def has_jotpk_power_level(self, power_level: int) -> StardewRule:
if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling:
if self.options.arcade_machine_locations != ArcadeMachineLocations.option_full_shuffling:
return True_()
jotpk_buffs = ["JotPK: Progressive Boots", "JotPK: Progressive Gun",
"JotPK: Progressive Ammo", "JotPK: Extra Life", "JotPK: Increased Drop Rate"]
return self.received(jotpk_buffs, power_level)
def has_junimo_kart_power_level(self, power_level: int) -> StardewRule:
if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling:
if self.options.arcade_machine_locations != ArcadeMachineLocations.option_full_shuffling:
return True_()
return self.received("Junimo Kart: Extra Life", power_level)
def has_junimo_kart_max_level(self) -> StardewRule:
play_rule = self.can_reach_region(Region.junimo_kart_3)
if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling:
if self.options.arcade_machine_locations != ArcadeMachineLocations.option_full_shuffling:
return play_rule
return self.has_junimo_kart_power_level(8)
@ -1043,12 +1042,12 @@ class StardewLogic:
def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule:
if hearts <= 0:
return True_()
friendsanity = self.options[options.Friendsanity]
if friendsanity == options.Friendsanity.option_none:
friendsanity = self.options.friendsanity
if friendsanity == Friendsanity.option_none:
return self.can_earn_relationship(npc, hearts)
if npc not in all_villagers_by_name:
if npc == NPC.pet:
if friendsanity == options.Friendsanity.option_bachelors:
if friendsanity == Friendsanity.option_bachelors:
return self.can_befriend_pet(hearts)
return self.received_hearts(NPC.pet, hearts)
if npc == Generic.any or npc == Generic.bachelor:
@ -1078,11 +1077,11 @@ class StardewLogic:
if not self.npc_is_in_current_slot(npc):
return True_()
villager = all_villagers_by_name[npc]
if friendsanity == options.Friendsanity.option_bachelors and not villager.bachelor:
if friendsanity == Friendsanity.option_bachelors and not villager.bachelor:
return self.can_earn_relationship(npc, hearts)
if friendsanity == options.Friendsanity.option_starting_npcs and not villager.available:
if friendsanity == Friendsanity.option_starting_npcs and not villager.available:
return self.can_earn_relationship(npc, hearts)
is_capped_at_8 = villager.bachelor and friendsanity != options.Friendsanity.option_all_with_marriage
is_capped_at_8 = villager.bachelor and friendsanity != Friendsanity.option_all_with_marriage
if is_capped_at_8 and hearts > 8:
return self.received_hearts(villager, 8) & self.can_earn_relationship(npc, hearts)
return self.received_hearts(villager, hearts)
@ -1090,7 +1089,7 @@ class StardewLogic:
def received_hearts(self, npc: Union[str, Villager], hearts: int) -> StardewRule:
if isinstance(npc, Villager):
return self.received_hearts(npc.name, hearts)
heart_size: int = self.options[options.FriendsanityHeartSize]
heart_size = self.options.friendsanity_heart_size.value
return self.received(self.heart(npc), math.ceil(hearts / heart_size))
def can_meet(self, npc: str) -> StardewRule:
@ -1122,13 +1121,13 @@ class StardewLogic:
if hearts <= 0:
return True_()
heart_size: int = self.options[options.FriendsanityHeartSize]
heart_size = self.options.friendsanity_heart_size.value
previous_heart = hearts - heart_size
previous_heart_rule = self.has_relationship(npc, previous_heart)
if npc == NPC.pet:
earn_rule = self.can_befriend_pet(hearts)
elif npc == NPC.wizard and ModNames.magic in self.options[options.Mods]:
elif npc == NPC.wizard and ModNames.magic in self.options.mods:
earn_rule = self.can_meet(npc) & self.has_lived_months(hearts)
elif npc in all_villagers_by_name:
if not self.npc_is_in_current_slot(npc):
@ -1284,7 +1283,7 @@ class StardewLogic:
return self.has_lived_months(8)
def can_speak_dwarf(self) -> StardewRule:
if self.options[options.Museumsanity] == options.Museumsanity.option_none:
if self.options.museumsanity == Museumsanity.option_none:
return And([self.can_donate_museum_item(item) for item in dwarf_scrolls])
return self.received("Dwarvish Translation Guide")
@ -1334,7 +1333,7 @@ class StardewLogic:
def can_complete_museum(self) -> StardewRule:
rules = [self.can_reach_region(Region.museum), self.can_mine_perfectly()]
if self.options[options.Museumsanity] != options.Museumsanity.option_none:
if self.options.museumsanity != Museumsanity.option_none:
rules.append(self.received("Traveling Merchant Metal Detector", 4))
for donation in all_museum_items:
@ -1345,9 +1344,9 @@ class StardewLogic:
if season == Generic.any:
return True_()
seasons_order = [Season.spring, Season.summer, Season.fall, Season.winter]
if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive:
if self.options.season_randomization == SeasonRandomization.option_progressive:
return self.received(Season.progressive, seasons_order.index(season))
if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled:
if self.options.season_randomization == SeasonRandomization.option_disabled:
if season == Season.spring:
return True_()
return self.has_lived_months(1)
@ -1371,19 +1370,19 @@ class StardewLogic:
return self.received("Month End", number)
def has_rusty_key(self) -> StardewRule:
if self.options[options.Museumsanity] == options.Museumsanity.option_none:
if self.options.museumsanity == Museumsanity.option_none:
required_donations = 80 # It's 60, but without a metal detector I'd rather overshoot so players don't get screwed by RNG
return self.has([item.name for item in all_museum_items], required_donations) & self.can_reach_region(Region.museum)
return self.received(Wallet.rusty_key)
def can_win_egg_hunt(self) -> StardewRule:
number_of_movement_buffs: int = self.options[options.NumberOfMovementBuffs]
if self.options[options.FestivalLocations] == options.FestivalLocations.option_hard or number_of_movement_buffs < 2:
number_of_movement_buffs = self.options.number_of_movement_buffs.value
if self.options.festival_locations == FestivalLocations.option_hard or number_of_movement_buffs < 2:
return True_()
return self.received(Buff.movement, number_of_movement_buffs // 2)
def can_succeed_luau_soup(self) -> StardewRule:
if self.options[options.FestivalLocations] != options.FestivalLocations.option_hard:
if self.options.festival_locations != FestivalLocations.option_hard:
return True_()
eligible_fish = [Fish.blobfish, Fish.crimsonfish, "Ice Pip", Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish,
Fish.mutant_carp, Fish.spookfish, Fish.stingray, Fish.sturgeon, "Super Cucumber"]
@ -1398,7 +1397,7 @@ class StardewLogic:
return Or(fish_rule) | Or(aged_rule)
def can_succeed_grange_display(self) -> StardewRule:
if self.options[options.FestivalLocations] != options.FestivalLocations.option_hard:
if self.options.festival_locations != FestivalLocations.option_hard:
return True_()
animal_rule = self.has_animal(Generic.any)
artisan_rule = self.can_keg(Generic.any) | self.can_preserves_jar(Generic.any)
@ -1527,12 +1526,12 @@ class StardewLogic:
return blacksmith_access & self.has(geode)
def has_island_trader(self) -> StardewRule:
if self.options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true:
if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return False_()
return self.can_reach_region(Region.island_trader)
def has_walnut(self, number: int) -> StardewRule:
if self.options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true:
if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return False_()
if number <= 0:
return True_()
@ -1592,7 +1591,7 @@ class StardewLogic:
def npc_is_in_current_slot(self, name: str) -> bool:
npc = all_villagers_by_name[name]
mod = npc.mod_name
return mod is None or mod in self.options[options.Mods]
return mod is None or mod in self.options.mods
def can_do_combat_at_level(self, level: str) -> StardewRule:
if level == Performance.basic:
@ -1612,7 +1611,7 @@ class StardewLogic:
return tool_rule | spell_rule
def has_prismatic_jelly_reward_access(self) -> StardewRule:
if self.options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled:
if self.options.special_order_locations == SpecialOrderLocations.option_disabled:
return self.can_complete_special_order("Prismatic Jelly")
return self.received("Monster Musk Recipe")

View File

@ -17,14 +17,14 @@ def can_reach_woods_depth(vanilla_logic, depth: int) -> StardewRule:
if depth > 50:
rules.append(vanilla_logic.can_do_combat_at_level(Performance.great) & vanilla_logic.can_cook() &
vanilla_logic.received(ModTransportation.woods_obelisk))
if vanilla_logic.options[options.SkillProgression] == options.SkillProgression.option_progressive:
if vanilla_logic.options.skill_progression == options.SkillProgression.option_progressive:
combat_tier = min(10, max(0, tier + 5))
rules.append(vanilla_logic.has_skill_level(Skill.combat, combat_tier))
return And(rules)
def has_woods_rune_to_depth(vanilla_logic, floor: int) -> StardewRule:
if vanilla_logic.options[options.ElevatorProgression] == options.ElevatorProgression.option_vanilla:
if vanilla_logic.options.elevator_progression == options.ElevatorProgression.option_vanilla:
return True_()
return vanilla_logic.received("Progressive Woods Obelisk Sigils", count=int(floor / 10))

View File

@ -7,19 +7,19 @@ from ... import options
def can_use_clear_debris_instead_of_tool_level(vanilla_logic, level: int) -> StardewRule:
if ModNames.magic not in vanilla_logic.options[options.Mods]:
if ModNames.magic not in vanilla_logic.options.mods:
return False_()
return vanilla_logic.received(MagicSpell.clear_debris) & can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, level)
def can_use_altar(vanilla_logic) -> StardewRule:
if ModNames.magic not in vanilla_logic.options[options.Mods]:
if ModNames.magic not in vanilla_logic.options.mods:
return False_()
return vanilla_logic.can_reach_region(MagicRegion.altar)
def has_any_spell(vanilla_logic) -> StardewRule:
if ModNames.magic not in vanilla_logic.options[options.Mods]:
if ModNames.magic not in vanilla_logic.options.mods:
return False_()
return can_use_altar(vanilla_logic)
@ -40,7 +40,7 @@ def has_support_spell_count(vanilla_logic, count: int) -> StardewRule:
def has_decent_spells(vanilla_logic) -> StardewRule:
if ModNames.magic not in vanilla_logic.options[options.Mods]:
if ModNames.magic not in vanilla_logic.options.mods:
return False_()
magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 2)
magic_attack_options_rule = has_attack_spell_count(vanilla_logic, 1)
@ -48,7 +48,7 @@ def has_decent_spells(vanilla_logic) -> StardewRule:
def has_good_spells(vanilla_logic) -> StardewRule:
if ModNames.magic not in vanilla_logic.options[options.Mods]:
if ModNames.magic not in vanilla_logic.options.mods:
return False_()
magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 4)
magic_attack_options_rule = has_attack_spell_count(vanilla_logic, 2)
@ -57,7 +57,7 @@ def has_good_spells(vanilla_logic) -> StardewRule:
def has_great_spells(vanilla_logic) -> StardewRule:
if ModNames.magic not in vanilla_logic.options[options.Mods]:
if ModNames.magic not in vanilla_logic.options.mods:
return False_()
magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 6)
magic_attack_options_rule = has_attack_spell_count(vanilla_logic, 3)
@ -66,7 +66,7 @@ def has_great_spells(vanilla_logic) -> StardewRule:
def has_amazing_spells(vanilla_logic) -> StardewRule:
if ModNames.magic not in vanilla_logic.options[options.Mods]:
if ModNames.magic not in vanilla_logic.options.mods:
return False_()
magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 8)
magic_attack_options_rule = has_attack_spell_count(vanilla_logic, 4)
@ -75,6 +75,6 @@ def has_amazing_spells(vanilla_logic) -> StardewRule:
def can_blink(vanilla_logic) -> StardewRule:
if ModNames.magic not in vanilla_logic.options[options.Mods]:
if ModNames.magic not in vanilla_logic.options.mods:
return False_()
return vanilla_logic.received(MagicSpell.blink) & can_use_altar(vanilla_logic)

View File

@ -29,17 +29,17 @@ def append_mod_skill_level(skills_items: List[str], active_mods):
def can_earn_mod_skill_level(logic, skill: str, level: int) -> StardewRule:
if ModNames.luck_skill in logic.options[options.Mods] and skill == ModSkill.luck:
if ModNames.luck_skill in logic.options.mods and skill == ModSkill.luck:
return can_earn_luck_skill_level(logic, level)
if ModNames.magic in logic.options[options.Mods] and skill == ModSkill.magic:
if ModNames.magic in logic.options.mods and skill == ModSkill.magic:
return can_earn_magic_skill_level(logic, level)
if ModNames.socializing_skill in logic.options[options.Mods] and skill == ModSkill.socializing:
if ModNames.socializing_skill in logic.options.mods and skill == ModSkill.socializing:
return can_earn_socializing_skill_level(logic, level)
if ModNames.archaeology in logic.options[options.Mods] and skill == ModSkill.archaeology:
if ModNames.archaeology in logic.options.mods and skill == ModSkill.archaeology:
return can_earn_archaeology_skill_level(logic, level)
if ModNames.cooking_skill in logic.options[options.Mods] and skill == ModSkill.cooking:
if ModNames.cooking_skill in logic.options.mods and skill == ModSkill.cooking:
return can_earn_cooking_skill_level(logic, level)
if ModNames.binning_skill in logic.options[options.Mods] and skill == ModSkill.binning:
if ModNames.binning_skill in logic.options.mods and skill == ModSkill.binning:
return can_earn_binning_skill_level(logic, level)
return False_()
@ -65,7 +65,7 @@ def can_earn_magic_skill_level(vanilla_logic, level: int) -> StardewRule:
def can_earn_socializing_skill_level(vanilla_logic, level: int) -> StardewRule:
villager_count = []
for villager in all_villagers:
if villager.mod_name in vanilla_logic.options[options.Mods] or villager.mod_name is None:
if villager.mod_name in vanilla_logic.options.mods or villager.mod_name is None:
villager_count.append(vanilla_logic.can_earn_relationship(villager.name, level))
return Count(level * 2, villager_count)

View File

@ -4,7 +4,7 @@ from ... import options
def has_skull_cavern_elevator_to_floor(self, floor: int) -> StardewRule:
if self.options[options.ElevatorProgression] != options.ElevatorProgression.option_vanilla and \
ModNames.skull_cavern_elevator in self.options[options.Mods]:
if self.options.elevator_progression != options.ElevatorProgression.option_vanilla and \
ModNames.skull_cavern_elevator in self.options.mods:
return self.received("Progressive Skull Cavern Elevator", floor // 25)
return True_()

View File

@ -1,29 +1,9 @@
from dataclasses import dataclass
from typing import Dict, Union, Protocol, runtime_checkable, ClassVar
from typing import Dict
from Options import Option, Range, DeathLink, SpecialRange, Toggle, Choice, OptionSet
from Options import Range, SpecialRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, Option
from .mods.mod_data import ModNames
@runtime_checkable
class StardewOption(Protocol):
internal_name: ClassVar[str]
@dataclass
class StardewOptions:
options: Dict[str, Union[bool, int, str]]
def __getitem__(self, item: Union[str, StardewOption]) -> Union[bool, int, str]:
if isinstance(item, StardewOption):
item = item.internal_name
return self.options.get(item, None)
def __setitem__(self, key: Union[str, StardewOption], value: Union[bool, int, str]):
if isinstance(key, StardewOption):
key = key.internal_name
self.options[key] = value
class Goal(Choice):
"""What's your goal with this play-through?
@ -553,56 +533,39 @@ class Mods(OptionSet):
}
stardew_valley_option_classes = [
Goal,
StartingMoney,
ProfitMargin,
BundleRandomization,
BundlePrice,
EntranceRandomization,
SeasonRandomization,
Cropsanity,
BackpackProgression,
ToolProgression,
SkillProgression,
BuildingProgression,
FestivalLocations,
ElevatorProgression,
ArcadeMachineLocations,
SpecialOrderLocations,
HelpWantedLocations,
Fishsanity,
Museumsanity,
Friendsanity,
FriendsanityHeartSize,
NumberOfMovementBuffs,
NumberOfLuckBuffs,
ExcludeGingerIsland,
TrapItems,
MultipleDaySleepEnabled,
MultipleDaySleepCost,
ExperienceMultiplier,
FriendshipMultiplier,
DebrisMultiplier,
QuickStart,
Gifting,
Mods,
]
stardew_valley_options: Dict[str, type(Option)] = {option.internal_name: option for option in
stardew_valley_option_classes}
default_options = {option.internal_name: option.default for option in stardew_valley_options.values()}
stardew_valley_options["death_link"] = DeathLink
def fetch_options(world, player: int) -> StardewOptions:
return StardewOptions({option: get_option_value(world, player, option) for option in stardew_valley_options})
def get_option_value(world, player: int, name: str) -> Union[bool, int]:
assert name in stardew_valley_options, f"{name} is not a valid option for Stardew Valley."
value = getattr(world, name)
if issubclass(stardew_valley_options[name], Toggle):
return bool(value[player].value)
return value[player].value
@dataclass
class StardewValleyOptions(PerGameCommonOptions):
goal: Goal
starting_money: StartingMoney
profit_margin: ProfitMargin
bundle_randomization: BundleRandomization
bundle_price: BundlePrice
entrance_randomization: EntranceRandomization
season_randomization: SeasonRandomization
cropsanity: Cropsanity
backpack_progression: BackpackProgression
tool_progression: ToolProgression
skill_progression: SkillProgression
building_progression: BuildingProgression
festival_locations: FestivalLocations
elevator_progression: ElevatorProgression
arcade_machine_locations: ArcadeMachineLocations
special_order_locations: SpecialOrderLocations
help_wanted_locations: HelpWantedLocations
fishsanity: Fishsanity
museumsanity: Museumsanity
friendsanity: Friendsanity
friendsanity_heart_size: FriendsanityHeartSize
number_of_movement_buffs: NumberOfMovementBuffs
number_of_luck_buffs: NumberOfLuckBuffs
exclude_ginger_island: ExcludeGingerIsland
trap_items: TrapItems
multiple_day_sleep_enabled: MultipleDaySleepEnabled
multiple_day_sleep_cost: MultipleDaySleepCost
experience_multiplier: ExperienceMultiplier
friendship_multiplier: FriendshipMultiplier
debris_multiplier: DebrisMultiplier
quick_start: QuickStart
gifting: Gifting
mods: Mods
death_link: DeathLink

View File

@ -2,11 +2,10 @@ from random import Random
from typing import Iterable, Dict, Protocol, List, Tuple, Set
from BaseClasses import Region, Entrance
from . import options
from .options import EntranceRandomization, ExcludeGingerIsland, Museumsanity
from .strings.entrance_names import Entrance
from .strings.region_names import Region
from .region_classes import RegionData, ConnectionData, RandomizationFlag
from .options import StardewOptions
from .mods.mod_regions import ModDataList
@ -397,12 +396,12 @@ vanilla_connections = [
]
def create_final_regions(world_options: StardewOptions) -> List[RegionData]:
def create_final_regions(world_options) -> List[RegionData]:
final_regions = []
final_regions.extend(vanilla_regions)
if world_options[options.Mods] is None:
if world_options.mods is None:
return final_regions
for mod in world_options[options.Mods]:
for mod in world_options.mods.value:
if mod not in ModDataList:
continue
for mod_region in ModDataList[mod].regions:
@ -417,19 +416,19 @@ def create_final_regions(world_options: StardewOptions) -> List[RegionData]:
return final_regions
def create_final_connections(world_options: StardewOptions) -> List[ConnectionData]:
def create_final_connections(world_options) -> List[ConnectionData]:
final_connections = []
final_connections.extend(vanilla_connections)
if world_options[options.Mods] is None:
if world_options.mods is None:
return final_connections
for mod in world_options[options.Mods]:
for mod in world_options.mods.value:
if mod not in ModDataList:
continue
final_connections.extend(ModDataList[mod].connections)
return final_connections
def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewOptions) -> Tuple[
def create_regions(region_factory: RegionFactory, random: Random, world_options) -> Tuple[
Iterable[Region], Dict[str, str]]:
final_regions = create_final_regions(world_options)
regions: Dict[str: Region] = {region.name: region_factory(region.name, region.exits) for region in
@ -448,21 +447,21 @@ def create_regions(region_factory: RegionFactory, random: Random, world_options:
return regions.values(), randomized_data
def randomize_connections(random: Random, world_options: StardewOptions, regions_by_name) -> Tuple[
def randomize_connections(random: Random, world_options, regions_by_name) -> Tuple[
List[ConnectionData], Dict[str, str]]:
connections_to_randomize = []
final_connections = create_final_connections(world_options)
connections_by_name: Dict[str, ConnectionData] = {connection.name: connection for connection in final_connections}
if world_options[options.EntranceRandomization] == options.EntranceRandomization.option_pelican_town:
if world_options.entrance_randomization == EntranceRandomization.option_pelican_town:
connections_to_randomize = [connection for connection in final_connections if
RandomizationFlag.PELICAN_TOWN in connection.flag]
elif world_options[options.EntranceRandomization] == options.EntranceRandomization.option_non_progression:
elif world_options.entrance_randomization == EntranceRandomization.option_non_progression:
connections_to_randomize = [connection for connection in final_connections if
RandomizationFlag.NON_PROGRESSION in connection.flag]
elif world_options[options.EntranceRandomization] == options.EntranceRandomization.option_buildings:
elif world_options.entrance_randomization == EntranceRandomization.option_buildings:
connections_to_randomize = [connection for connection in final_connections if
RandomizationFlag.BUILDINGS in connection.flag]
elif world_options[options.EntranceRandomization] == options.EntranceRandomization.option_chaos:
elif world_options.entrance_randomization == EntranceRandomization.option_chaos:
connections_to_randomize = [connection for connection in final_connections if
RandomizationFlag.BUILDINGS in connection.flag]
connections_to_randomize = exclude_island_if_necessary(connections_to_randomize, world_options)
@ -491,8 +490,8 @@ def randomize_connections(random: Random, world_options: StardewOptions, regions
def remove_excluded_entrances(connections_to_randomize, world_options):
exclude_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true
exclude_sewers = world_options[options.Museumsanity] == options.Museumsanity.option_none
exclude_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_true
exclude_sewers = world_options.museumsanity == Museumsanity.option_none
if exclude_island:
connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.GINGER_ISLAND not in connection.flag]
if exclude_sewers:
@ -502,7 +501,7 @@ def remove_excluded_entrances(connections_to_randomize, world_options):
def exclude_island_if_necessary(connections_to_randomize: List[ConnectionData], world_options) -> List[ConnectionData]:
exclude_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true
exclude_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_true
if exclude_island:
connections_to_randomize = [connection for connection in connections_to_randomize if
RandomizationFlag.GINGER_ISLAND not in connection.flag]

View File

@ -1,10 +1,10 @@
import itertools
from typing import Dict, List
from typing import List
from BaseClasses import MultiWorld
from worlds.generic import Rules as MultiWorldRules
from . import options, locations
from .bundles import Bundle
from .options import StardewValleyOptions, ToolProgression, BuildingProgression, SkillProgression, ExcludeGingerIsland, Cropsanity, SpecialOrderLocations, Museumsanity, \
BackpackProgression, ArcadeMachineLocations
from .strings.entrance_names import dig_to_mines_floor, dig_to_skull_floor, Entrance, move_to_woods_depth, \
DeepWoodsEntrance, AlecEntrance, MagicEntrance
from .data.museum_data import all_museum_items, all_museum_minerals, all_museum_artifacts, \
@ -13,9 +13,8 @@ from .data.museum_data import all_museum_items, all_museum_minerals, all_museum_
from .strings.region_names import Region
from .mods.mod_data import ModNames
from .mods.logic import magic, deepwoods
from .locations import LocationTags
from .locations import LocationTags, locations_by_tag
from .logic import StardewLogic, And, tool_upgrade_prices
from .options import StardewOptions
from .strings.ap_names.transport_names import Transportation
from .strings.artisan_good_names import ArtisanGood
from .strings.calendar_names import Weekday
@ -28,251 +27,256 @@ from .strings.villager_names import NPC, ModNPC
from .strings.wallet_item_names import Wallet
def set_rules(multi_world: MultiWorld, player: int, world_options: StardewOptions, logic: StardewLogic,
current_bundles: Dict[str, Bundle]):
all_location_names = list(location.name for location in multi_world.get_locations(player))
def set_rules(world):
multiworld = world.multiworld
world_options = world.options
player = world.player
logic = world.logic
current_bundles = world.modified_bundles
all_location_names = list(location.name for location in multiworld.get_locations(player))
set_entrance_rules(logic, multi_world, player, world_options)
set_entrance_rules(logic, multiworld, player, world_options)
set_ginger_island_rules(logic, multi_world, player, world_options)
set_ginger_island_rules(logic, multiworld, player, world_options)
# Those checks do not exist if ToolProgression is vanilla
if world_options[options.ToolProgression] != options.ToolProgression.option_vanilla:
MultiWorldRules.add_rule(multi_world.get_location("Purchase Fiberglass Rod", player),
if world_options.tool_progression != ToolProgression.option_vanilla:
MultiWorldRules.add_rule(multiworld.get_location("Purchase Fiberglass Rod", player),
(logic.has_skill_level(Skill.fishing, 2) & logic.can_spend_money(1800)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Purchase Iridium Rod", player),
MultiWorldRules.add_rule(multiworld.get_location("Purchase Iridium Rod", player),
(logic.has_skill_level(Skill.fishing, 6) & logic.can_spend_money(7500)).simplify())
materials = [None, "Copper", "Iron", "Gold", "Iridium"]
tool = [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.watering_can, Tool.trash_can]
for (previous, material), tool in itertools.product(zip(materials[:4], materials[1:]), tool):
if previous is None:
MultiWorldRules.add_rule(multi_world.get_location(f"{material} {tool} Upgrade", player),
MultiWorldRules.add_rule(multiworld.get_location(f"{material} {tool} Upgrade", player),
(logic.has(f"{material} Ore") &
logic.can_spend_money(tool_upgrade_prices[material])).simplify())
else:
MultiWorldRules.add_rule(multi_world.get_location(f"{material} {tool} Upgrade", player),
MultiWorldRules.add_rule(multiworld.get_location(f"{material} {tool} Upgrade", player),
(logic.has(f"{material} Ore") & logic.has_tool(tool, previous) &
logic.can_spend_money(tool_upgrade_prices[material])).simplify())
set_skills_rules(logic, multi_world, player, world_options)
set_skills_rules(logic, multiworld, player, world_options)
# Bundles
for bundle in current_bundles.values():
location = multi_world.get_location(bundle.get_name_with_bundle(), player)
location = multiworld.get_location(bundle.get_name_with_bundle(), player)
rules = logic.can_complete_bundle(bundle.requirements, bundle.number_required)
simplified_rules = rules.simplify()
MultiWorldRules.set_rule(location, simplified_rules)
MultiWorldRules.add_rule(multi_world.get_location("Complete Crafts Room", player),
MultiWorldRules.add_rule(multiworld.get_location("Complete Crafts Room", player),
And(logic.can_reach_location(bundle.name)
for bundle in locations.locations_by_tag[LocationTags.CRAFTS_ROOM_BUNDLE]).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Complete Pantry", player),
for bundle in locations_by_tag[LocationTags.CRAFTS_ROOM_BUNDLE]).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Complete Pantry", player),
And(logic.can_reach_location(bundle.name)
for bundle in locations.locations_by_tag[LocationTags.PANTRY_BUNDLE]).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Complete Fish Tank", player),
for bundle in locations_by_tag[LocationTags.PANTRY_BUNDLE]).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Complete Fish Tank", player),
And(logic.can_reach_location(bundle.name)
for bundle in locations.locations_by_tag[LocationTags.FISH_TANK_BUNDLE]).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Complete Boiler Room", player),
for bundle in locations_by_tag[LocationTags.FISH_TANK_BUNDLE]).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Complete Boiler Room", player),
And(logic.can_reach_location(bundle.name)
for bundle in locations.locations_by_tag[LocationTags.BOILER_ROOM_BUNDLE]).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Complete Bulletin Board", player),
for bundle in locations_by_tag[LocationTags.BOILER_ROOM_BUNDLE]).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Complete Bulletin Board", player),
And(logic.can_reach_location(bundle.name)
for bundle
in locations.locations_by_tag[LocationTags.BULLETIN_BOARD_BUNDLE]).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Complete Vault", player),
in locations_by_tag[LocationTags.BULLETIN_BOARD_BUNDLE]).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Complete Vault", player),
And(logic.can_reach_location(bundle.name)
for bundle in locations.locations_by_tag[LocationTags.VAULT_BUNDLE]).simplify())
for bundle in locations_by_tag[LocationTags.VAULT_BUNDLE]).simplify())
# Buildings
if world_options[options.BuildingProgression] != options.BuildingProgression.option_vanilla:
for building in locations.locations_by_tag[LocationTags.BUILDING_BLUEPRINT]:
if building.mod_name is not None and building.mod_name not in world_options[options.Mods]:
if world_options.building_progression != BuildingProgression.option_vanilla:
for building in locations_by_tag[LocationTags.BUILDING_BLUEPRINT]:
if building.mod_name is not None and building.mod_name not in world_options.mods:
continue
MultiWorldRules.set_rule(multi_world.get_location(building.name, player),
MultiWorldRules.set_rule(multiworld.get_location(building.name, player),
logic.building_rules[building.name.replace(" Blueprint", "")].simplify())
set_cropsanity_rules(all_location_names, logic, multi_world, player, world_options)
set_story_quests_rules(all_location_names, logic, multi_world, player, world_options)
set_special_order_rules(all_location_names, logic, multi_world, player, world_options)
set_help_wanted_quests_rules(logic, multi_world, player, world_options)
set_fishsanity_rules(all_location_names, logic, multi_world, player)
set_museumsanity_rules(all_location_names, logic, multi_world, player, world_options)
set_friendsanity_rules(all_location_names, logic, multi_world, player)
set_backpack_rules(logic, multi_world, player, world_options)
set_festival_rules(all_location_names, logic, multi_world, player)
set_cropsanity_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_help_wanted_quests_rules(logic, multiworld, player, world_options)
set_fishsanity_rules(all_location_names, logic, multiworld, player)
set_museumsanity_rules(all_location_names, logic, multiworld, player, world_options)
set_friendsanity_rules(all_location_names, logic, multiworld, player)
set_backpack_rules(logic, multiworld, player, world_options)
set_festival_rules(all_location_names, logic, multiworld, player)
MultiWorldRules.add_rule(multi_world.get_location("Old Master Cannoli", player),
MultiWorldRules.add_rule(multiworld.get_location("Old Master Cannoli", player),
logic.has("Sweet Gem Berry").simplify())
MultiWorldRules.add_rule(multi_world.get_location("Galaxy Sword Shrine", player),
MultiWorldRules.add_rule(multiworld.get_location("Galaxy Sword Shrine", player),
logic.has("Prismatic Shard").simplify())
MultiWorldRules.add_rule(multi_world.get_location("Have a Baby", player),
MultiWorldRules.add_rule(multiworld.get_location("Have a Baby", player),
logic.can_reproduce(1).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Have Another Baby", player),
MultiWorldRules.add_rule(multiworld.get_location("Have Another Baby", player),
logic.can_reproduce(2).simplify())
set_traveling_merchant_rules(logic, multi_world, player)
set_arcade_machine_rules(logic, multi_world, player, world_options)
set_deepwoods_rules(logic, multi_world, player, world_options)
set_magic_spell_rules(logic, multi_world, player, world_options)
set_traveling_merchant_rules(logic, multiworld, player)
set_arcade_machine_rules(logic, multiworld, player, world_options)
set_deepwoods_rules(logic, multiworld, player, world_options)
set_magic_spell_rules(logic, multiworld, player, world_options)
def set_skills_rules(logic, multi_world, player, world_options):
def set_skills_rules(logic, multiworld, player, world_options):
# Skills
if world_options[options.SkillProgression] != options.SkillProgression.option_vanilla:
if world_options.skill_progression != SkillProgression.option_vanilla:
for i in range(1, 11):
set_skill_rule(logic, multi_world, player, Skill.farming, i)
set_skill_rule(logic, multi_world, player, Skill.fishing, i)
set_skill_rule(logic, multi_world, player, Skill.foraging, i)
set_skill_rule(logic, multi_world, player, Skill.mining, i)
set_skill_rule(logic, multi_world, player, Skill.combat, i)
set_skill_rule(logic, multiworld, player, Skill.farming, i)
set_skill_rule(logic, multiworld, player, Skill.fishing, i)
set_skill_rule(logic, multiworld, player, Skill.foraging, i)
set_skill_rule(logic, multiworld, player, Skill.mining, i)
set_skill_rule(logic, multiworld, player, Skill.combat, i)
# Modded Skills
if ModNames.luck_skill in world_options[options.Mods]:
set_skill_rule(logic, multi_world, player, ModSkill.luck, i)
if ModNames.magic in world_options[options.Mods]:
set_skill_rule(logic, multi_world, player, ModSkill.magic, i)
if ModNames.binning_skill in world_options[options.Mods]:
set_skill_rule(logic, multi_world, player, ModSkill.binning, i)
if ModNames.cooking_skill in world_options[options.Mods]:
set_skill_rule(logic, multi_world, player, ModSkill.cooking, i)
if ModNames.socializing_skill in world_options[options.Mods]:
set_skill_rule(logic, multi_world, player, ModSkill.socializing, i)
if ModNames.archaeology in world_options[options.Mods]:
set_skill_rule(logic, multi_world, player, ModSkill.archaeology, i)
if ModNames.luck_skill in world_options.mods:
set_skill_rule(logic, multiworld, player, ModSkill.luck, i)
if ModNames.magic in world_options.mods:
set_skill_rule(logic, multiworld, player, ModSkill.magic, i)
if ModNames.binning_skill in world_options.mods:
set_skill_rule(logic, multiworld, player, ModSkill.binning, i)
if ModNames.cooking_skill in world_options.mods:
set_skill_rule(logic, multiworld, player, ModSkill.cooking, i)
if ModNames.socializing_skill in world_options.mods:
set_skill_rule(logic, multiworld, player, ModSkill.socializing, i)
if ModNames.archaeology in world_options.mods:
set_skill_rule(logic, multiworld, player, ModSkill.archaeology, i)
def set_skill_rule(logic, multi_world, player, skill: str, level: int):
def set_skill_rule(logic, multiworld, player, skill: str, level: int):
location_name = f"Level {level} {skill}"
location = multi_world.get_location(location_name, player)
location = multiworld.get_location(location_name, player)
rule = logic.can_earn_skill_level(skill, level).simplify()
MultiWorldRules.set_rule(location, rule)
def set_entrance_rules(logic, multi_world, player, world_options: StardewOptions):
def set_entrance_rules(logic, multiworld, player, world_options: StardewValleyOptions):
for floor in range(5, 120 + 5, 5):
MultiWorldRules.set_rule(multi_world.get_entrance(dig_to_mines_floor(floor), player),
MultiWorldRules.set_rule(multiworld.get_entrance(dig_to_mines_floor(floor), player),
logic.can_mine_to_floor(floor).simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_tide_pools, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_tide_pools, player),
logic.received("Beach Bridge") | (magic.can_blink(logic)).simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_quarry, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_quarry, player),
logic.received("Bridge Repair") | (magic.can_blink(logic)).simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_secret_woods, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_secret_woods, player),
logic.has_tool(Tool.axe, "Iron") | (magic.can_blink(logic)).simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.forest_to_sewer, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.forest_to_sewer, player),
logic.has_rusty_key().simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.town_to_sewer, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.town_to_sewer, player),
logic.has_rusty_key().simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.take_bus_to_desert, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.take_bus_to_desert, player),
logic.received("Bus Repair").simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_skull_cavern, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_skull_cavern, player),
logic.received(Wallet.skull_key).simplify())
for floor in range(25, 200 + 25, 25):
MultiWorldRules.set_rule(multi_world.get_entrance(dig_to_skull_floor(floor), player),
MultiWorldRules.set_rule(multiworld.get_entrance(dig_to_skull_floor(floor), player),
logic.can_mine_to_skull_cavern_floor(floor).simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.talk_to_mines_dwarf, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.talk_to_mines_dwarf, player),
logic.can_speak_dwarf() & logic.has_tool(Tool.pickaxe, ToolMaterial.iron))
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.use_desert_obelisk, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.use_desert_obelisk, player),
logic.received(Transportation.desert_obelisk).simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.use_island_obelisk, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.use_island_obelisk, player),
logic.received(Transportation.island_obelisk).simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.use_farm_obelisk, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.use_farm_obelisk, player),
logic.received(Transportation.farm_obelisk).simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.buy_from_traveling_merchant, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.buy_from_traveling_merchant, player),
logic.has_traveling_merchant())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_greenhouse, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_greenhouse, player),
logic.received("Greenhouse"))
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.mountain_to_adventurer_guild, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.mountain_to_adventurer_guild, player),
logic.received("Adventurer's Guild"))
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.mountain_to_railroad, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.mountain_to_railroad, player),
logic.has_lived_months(2))
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_witch_warp_cave, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_witch_warp_cave, player),
logic.received(Wallet.dark_talisman) | (magic.can_blink(logic)).simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_witch_hut, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_witch_hut, player),
(logic.has(ArtisanGood.void_mayonnaise) | magic.can_blink(logic)).simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_mutant_bug_lair, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_mutant_bug_lair, player),
((logic.has_rusty_key() & logic.can_reach_region(Region.railroad) &
logic.can_meet(NPC.krobus) | magic.can_blink(logic)).simplify()))
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_harvey_room, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_harvey_room, player),
logic.has_relationship(NPC.harvey, 2))
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.mountain_to_maru_room, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.mountain_to_maru_room, player),
logic.has_relationship(NPC.maru, 2))
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_sebastian_room, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_sebastian_room, player),
(logic.has_relationship(NPC.sebastian, 2) | magic.can_blink(logic)).simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.forest_to_leah_cottage, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.forest_to_leah_cottage, player),
logic.has_relationship(NPC.leah, 2))
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_elliott_house, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_elliott_house, player),
logic.has_relationship(NPC.elliott, 2))
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_sunroom, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_sunroom, player),
logic.has_relationship(NPC.caroline, 2))
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_wizard_basement, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_wizard_basement, player),
logic.has_relationship(NPC.wizard, 4))
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.mountain_to_leo_treehouse, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.mountain_to_leo_treehouse, player),
logic.received("Treehouse"))
if ModNames.alec in world_options[options.Mods]:
MultiWorldRules.set_rule(multi_world.get_entrance(AlecEntrance.petshop_to_bedroom, player),
if ModNames.alec in world_options.mods:
MultiWorldRules.set_rule(multiworld.get_entrance(AlecEntrance.petshop_to_bedroom, player),
(logic.has_relationship(ModNPC.alec, 2) | magic.can_blink(logic)).simplify())
def set_ginger_island_rules(logic: StardewLogic, multi_world, player, world_options: StardewOptions):
set_island_entrances_rules(logic, multi_world, player)
if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true:
def set_ginger_island_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
set_island_entrances_rules(logic, multiworld, player)
if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return
set_boat_repair_rules(logic, multi_world, player)
set_island_parrot_rules(logic, multi_world, player)
MultiWorldRules.add_rule(multi_world.get_location("Open Professor Snail Cave", player),
set_boat_repair_rules(logic, multiworld, player)
set_island_parrot_rules(logic, multiworld, player)
MultiWorldRules.add_rule(multiworld.get_location("Open Professor Snail Cave", player),
logic.has(Craftable.cherry_bomb).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Complete Island Field Office", player),
MultiWorldRules.add_rule(multiworld.get_location("Complete Island Field Office", player),
logic.can_complete_field_office().simplify())
def set_boat_repair_rules(logic: StardewLogic, multi_world, player):
MultiWorldRules.add_rule(multi_world.get_location("Repair Boat Hull", player),
def set_boat_repair_rules(logic: StardewLogic, multiworld, player):
MultiWorldRules.add_rule(multiworld.get_location("Repair Boat Hull", player),
logic.has(Material.hardwood).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Repair Boat Anchor", player),
MultiWorldRules.add_rule(multiworld.get_location("Repair Boat Anchor", player),
logic.has(MetalBar.iridium).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Repair Ticket Machine", player),
MultiWorldRules.add_rule(multiworld.get_location("Repair Ticket Machine", player),
logic.has(ArtisanGood.battery_pack).simplify())
def set_island_entrances_rules(logic: StardewLogic, multi_world, player):
def set_island_entrances_rules(logic: StardewLogic, multiworld, player):
boat_repaired = logic.received(Transportation.boat_repair).simplify()
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.fish_shop_to_boat_tunnel, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.fish_shop_to_boat_tunnel, player),
boat_repaired)
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.boat_to_ginger_island, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.boat_to_ginger_island, player),
boat_repaired)
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_south_to_west, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_south_to_west, player),
logic.received("Island West Turtle").simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_south_to_north, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_south_to_north, player),
logic.received("Island North Turtle").simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_west_to_islandfarmhouse, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_west_to_islandfarmhouse, player),
logic.received("Island Farmhouse").simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_west_to_gourmand_cave, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_west_to_gourmand_cave, player),
logic.received("Island Farmhouse").simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_north_to_dig_site, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_north_to_dig_site, player),
logic.received("Dig Site Bridge").simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.dig_site_to_professor_snail_cave, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.dig_site_to_professor_snail_cave, player),
logic.received("Open Professor Snail Cave").simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.talk_to_island_trader, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.talk_to_island_trader, player),
logic.received("Island Trader").simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_south_to_southeast, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_south_to_southeast, player),
logic.received("Island Resort").simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.use_island_resort, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.use_island_resort, player),
logic.received("Island Resort").simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_west_to_qi_walnut_room, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_west_to_qi_walnut_room, player),
logic.received("Qi Walnut Room").simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_north_to_volcano, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_north_to_volcano, player),
(logic.can_water(0) | logic.received("Volcano Bridge") |
magic.can_blink(logic)).simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.volcano_to_secret_beach, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.volcano_to_secret_beach, player),
logic.can_water(2).simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.climb_to_volcano_5, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.climb_to_volcano_5, player),
(logic.can_mine_perfectly() & logic.can_water(1)).simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.talk_to_volcano_dwarf, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.talk_to_volcano_dwarf, player),
logic.can_speak_dwarf())
MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.climb_to_volcano_10, player),
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.climb_to_volcano_10, player),
(logic.can_mine_perfectly() & logic.can_water(1) & logic.received("Volcano Exit Shortcut")).simplify())
parrots = [Entrance.parrot_express_docks_to_volcano, Entrance.parrot_express_jungle_to_volcano,
Entrance.parrot_express_dig_site_to_volcano, Entrance.parrot_express_docks_to_dig_site,
@ -281,78 +285,78 @@ def set_island_entrances_rules(logic: StardewLogic, multi_world, player):
Entrance.parrot_express_volcano_to_jungle, Entrance.parrot_express_jungle_to_docks,
Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_volcano_to_docks]
for parrot in parrots:
MultiWorldRules.set_rule(multi_world.get_entrance(parrot, player), logic.received(Transportation.parrot_express).simplify())
MultiWorldRules.set_rule(multiworld.get_entrance(parrot, player), logic.received(Transportation.parrot_express).simplify())
def set_island_parrot_rules(logic: StardewLogic, multi_world, player):
def set_island_parrot_rules(logic: StardewLogic, multiworld, player):
has_walnut = logic.has_walnut(1).simplify()
has_5_walnut = logic.has_walnut(5).simplify()
has_10_walnut = logic.has_walnut(10).simplify()
has_20_walnut = logic.has_walnut(20).simplify()
MultiWorldRules.add_rule(multi_world.get_location("Leo's Parrot", player),
MultiWorldRules.add_rule(multiworld.get_location("Leo's Parrot", player),
has_walnut)
MultiWorldRules.add_rule(multi_world.get_location("Island West Turtle", player),
MultiWorldRules.add_rule(multiworld.get_location("Island West Turtle", player),
has_10_walnut & logic.received("Island North Turtle"))
MultiWorldRules.add_rule(multi_world.get_location("Island Farmhouse", player),
MultiWorldRules.add_rule(multiworld.get_location("Island Farmhouse", player),
has_20_walnut)
MultiWorldRules.add_rule(multi_world.get_location("Island Mailbox", player),
MultiWorldRules.add_rule(multiworld.get_location("Island Mailbox", player),
has_5_walnut & logic.received("Island Farmhouse"))
MultiWorldRules.add_rule(multi_world.get_location(Transportation.farm_obelisk, player),
MultiWorldRules.add_rule(multiworld.get_location(Transportation.farm_obelisk, player),
has_20_walnut & logic.received("Island Mailbox"))
MultiWorldRules.add_rule(multi_world.get_location("Dig Site Bridge", player),
MultiWorldRules.add_rule(multiworld.get_location("Dig Site Bridge", player),
has_10_walnut & logic.received("Island West Turtle"))
MultiWorldRules.add_rule(multi_world.get_location("Island Trader", player),
MultiWorldRules.add_rule(multiworld.get_location("Island Trader", player),
has_10_walnut & logic.received("Island Farmhouse"))
MultiWorldRules.add_rule(multi_world.get_location("Volcano Bridge", player),
MultiWorldRules.add_rule(multiworld.get_location("Volcano Bridge", player),
has_5_walnut & logic.received("Island West Turtle") &
logic.can_reach_region(Region.volcano_floor_10))
MultiWorldRules.add_rule(multi_world.get_location("Volcano Exit Shortcut", player),
MultiWorldRules.add_rule(multiworld.get_location("Volcano Exit Shortcut", player),
has_5_walnut & logic.received("Island West Turtle"))
MultiWorldRules.add_rule(multi_world.get_location("Island Resort", player),
MultiWorldRules.add_rule(multiworld.get_location("Island Resort", player),
has_20_walnut & logic.received("Island Farmhouse"))
MultiWorldRules.add_rule(multi_world.get_location(Transportation.parrot_express, player),
MultiWorldRules.add_rule(multiworld.get_location(Transportation.parrot_express, player),
has_10_walnut)
def set_cropsanity_rules(all_location_names: List[str], logic, multi_world, player, world_options: StardewOptions):
if world_options[options.Cropsanity] == options.Cropsanity.option_disabled:
def set_cropsanity_rules(all_location_names: List[str], logic, multiworld, player, world_options: StardewValleyOptions):
if world_options.cropsanity == Cropsanity.option_disabled:
return
harvest_prefix = "Harvest "
harvest_prefix_length = len(harvest_prefix)
for harvest_location in locations.locations_by_tag[LocationTags.CROPSANITY]:
if harvest_location.name in all_location_names and (harvest_location.mod_name is None or harvest_location.mod_name in world_options[options.Mods]):
for harvest_location in locations_by_tag[LocationTags.CROPSANITY]:
if harvest_location.name in all_location_names and (harvest_location.mod_name is None or harvest_location.mod_name in world_options.mods):
crop_name = harvest_location.name[harvest_prefix_length:]
MultiWorldRules.set_rule(multi_world.get_location(harvest_location.name, player),
MultiWorldRules.set_rule(multiworld.get_location(harvest_location.name, player),
logic.has(crop_name).simplify())
def set_story_quests_rules(all_location_names: List[str], logic, multi_world, player, world_options: StardewOptions):
for quest in locations.locations_by_tag[LocationTags.QUEST]:
if quest.name in all_location_names and (quest.mod_name is None or quest.mod_name in world_options[options.Mods]):
MultiWorldRules.set_rule(multi_world.get_location(quest.name, player),
def set_story_quests_rules(all_location_names: List[str], logic, multiworld, player, world_options: StardewValleyOptions):
for quest in locations_by_tag[LocationTags.QUEST]:
if quest.name in all_location_names and (quest.mod_name is None or quest.mod_name in world_options.mods):
MultiWorldRules.set_rule(multiworld.get_location(quest.name, player),
logic.quest_rules[quest.name].simplify())
def set_special_order_rules(all_location_names: List[str], logic: StardewLogic, multi_world, player,
world_options: StardewOptions):
if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled:
def set_special_order_rules(all_location_names: List[str], logic: StardewLogic, multiworld, player,
world_options: StardewValleyOptions):
if world_options.special_order_locations == SpecialOrderLocations.option_disabled:
return
board_rule = logic.received("Special Order Board") & logic.has_lived_months(4)
for board_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]:
for board_order in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]:
if board_order.name in all_location_names:
order_rule = board_rule & logic.special_order_rules[board_order.name]
MultiWorldRules.set_rule(multi_world.get_location(board_order.name, player), order_rule.simplify())
MultiWorldRules.set_rule(multiworld.get_location(board_order.name, player), order_rule.simplify())
if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true:
if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return
if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_board_only:
if world_options.special_order_locations == SpecialOrderLocations.option_board_only:
return
qi_rule = logic.can_reach_region(Region.qi_walnut_room) & logic.has_lived_months(8)
for qi_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_QI]:
for qi_order in locations_by_tag[LocationTags.SPECIAL_ORDER_QI]:
if qi_order.name in all_location_names:
order_rule = qi_rule & logic.special_order_rules[qi_order.name]
MultiWorldRules.set_rule(multi_world.get_location(qi_order.name, player), order_rule.simplify())
MultiWorldRules.set_rule(multiworld.get_location(qi_order.name, player), order_rule.simplify())
help_wanted_prefix = "Help Wanted:"
@ -362,8 +366,8 @@ fishing = "Fishing"
slay_monsters = "Slay Monsters"
def set_help_wanted_quests_rules(logic: StardewLogic, multi_world, player, world_options):
help_wanted_number = world_options[options.HelpWantedLocations]
def set_help_wanted_quests_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
help_wanted_number = world_options.help_wanted_locations
for i in range(0, help_wanted_number):
set_number = i // 7
month_rule = logic.has_lived_months(set_number).simplify()
@ -371,58 +375,58 @@ def set_help_wanted_quests_rules(logic: StardewLogic, multi_world, player, world
quest_number_in_set = i % 7
if quest_number_in_set < 4:
quest_number = set_number * 4 + quest_number_in_set + 1
set_help_wanted_delivery_rule(multi_world, player, month_rule, quest_number)
set_help_wanted_delivery_rule(multiworld, player, month_rule, quest_number)
elif quest_number_in_set == 4:
set_help_wanted_fishing_rule(logic, multi_world, player, month_rule, quest_number)
set_help_wanted_fishing_rule(logic, multiworld, player, month_rule, quest_number)
elif quest_number_in_set == 5:
set_help_wanted_slay_monsters_rule(logic, multi_world, player, month_rule, quest_number)
set_help_wanted_slay_monsters_rule(logic, multiworld, player, month_rule, quest_number)
elif quest_number_in_set == 6:
set_help_wanted_gathering_rule(multi_world, player, month_rule, quest_number)
set_help_wanted_gathering_rule(multiworld, player, month_rule, quest_number)
def set_help_wanted_delivery_rule(multi_world, player, month_rule, quest_number):
def set_help_wanted_delivery_rule(multiworld, player, month_rule, quest_number):
location_name = f"{help_wanted_prefix} {item_delivery} {quest_number}"
MultiWorldRules.set_rule(multi_world.get_location(location_name, player), month_rule)
MultiWorldRules.set_rule(multiworld.get_location(location_name, player), month_rule)
def set_help_wanted_gathering_rule(multi_world, player, month_rule, quest_number):
def set_help_wanted_gathering_rule(multiworld, player, month_rule, quest_number):
location_name = f"{help_wanted_prefix} {gathering} {quest_number}"
MultiWorldRules.set_rule(multi_world.get_location(location_name, player), month_rule)
MultiWorldRules.set_rule(multiworld.get_location(location_name, player), month_rule)
def set_help_wanted_fishing_rule(logic: StardewLogic, multi_world, player, month_rule, quest_number):
def set_help_wanted_fishing_rule(logic: StardewLogic, multiworld, player, month_rule, quest_number):
location_name = f"{help_wanted_prefix} {fishing} {quest_number}"
fishing_rule = month_rule & logic.can_fish()
MultiWorldRules.set_rule(multi_world.get_location(location_name, player), fishing_rule.simplify())
MultiWorldRules.set_rule(multiworld.get_location(location_name, player), fishing_rule.simplify())
def set_help_wanted_slay_monsters_rule(logic: StardewLogic, multi_world, player, month_rule, quest_number):
def set_help_wanted_slay_monsters_rule(logic: StardewLogic, multiworld, player, month_rule, quest_number):
location_name = f"{help_wanted_prefix} {slay_monsters} {quest_number}"
slay_rule = month_rule & logic.can_do_combat_at_level("Basic")
MultiWorldRules.set_rule(multi_world.get_location(location_name, player), slay_rule.simplify())
MultiWorldRules.set_rule(multiworld.get_location(location_name, player), slay_rule.simplify())
def set_fishsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int):
def set_fishsanity_rules(all_location_names: List[str], logic: StardewLogic, multiworld: MultiWorld, player: int):
fish_prefix = "Fishsanity: "
for fish_location in locations.locations_by_tag[LocationTags.FISHSANITY]:
for fish_location in locations_by_tag[LocationTags.FISHSANITY]:
if fish_location.name in all_location_names:
fish_name = fish_location.name[len(fish_prefix):]
MultiWorldRules.set_rule(multi_world.get_location(fish_location.name, player),
MultiWorldRules.set_rule(multiworld.get_location(fish_location.name, player),
logic.has(fish_name).simplify())
def set_museumsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int,
world_options: StardewOptions):
def set_museumsanity_rules(all_location_names: List[str], logic: StardewLogic, multiworld: MultiWorld, player: int,
world_options: StardewValleyOptions):
museum_prefix = "Museumsanity: "
if world_options[options.Museumsanity] == options.Museumsanity.option_milestones:
for museum_milestone in locations.locations_by_tag[LocationTags.MUSEUM_MILESTONES]:
set_museum_milestone_rule(logic, multi_world, museum_milestone, museum_prefix, player)
elif world_options[options.Museumsanity] != options.Museumsanity.option_none:
set_museum_individual_donations_rules(all_location_names, logic, multi_world, museum_prefix, player)
if world_options.museumsanity == Museumsanity.option_milestones:
for museum_milestone in locations_by_tag[LocationTags.MUSEUM_MILESTONES]:
set_museum_milestone_rule(logic, multiworld, museum_milestone, museum_prefix, player)
elif world_options.museumsanity != Museumsanity.option_none:
set_museum_individual_donations_rules(all_location_names, logic, multiworld, museum_prefix, player)
def set_museum_individual_donations_rules(all_location_names, logic: StardewLogic, multi_world, museum_prefix, player):
all_donations = sorted(locations.locations_by_tag[LocationTags.MUSEUM_DONATIONS],
def set_museum_individual_donations_rules(all_location_names, logic: StardewLogic, multiworld, museum_prefix, player):
all_donations = sorted(locations_by_tag[LocationTags.MUSEUM_DONATIONS],
key=lambda x: all_museum_items_by_name[x.name[len(museum_prefix):]].difficulty, reverse=True)
counter = 0
number_donations = len(all_donations)
@ -430,13 +434,14 @@ def set_museum_individual_donations_rules(all_location_names, logic: StardewLogi
if museum_location.name in all_location_names:
donation_name = museum_location.name[len(museum_prefix):]
required_detectors = counter * 5 // number_donations
rule = logic.can_donate_museum_item(all_museum_items_by_name[donation_name]) & logic.received("Traveling Merchant Metal Detector", required_detectors)
MultiWorldRules.set_rule(multi_world.get_location(museum_location.name, player),
rule = logic.can_donate_museum_item(all_museum_items_by_name[donation_name]) & logic.received("Traveling Merchant Metal Detector",
required_detectors)
MultiWorldRules.set_rule(multiworld.get_location(museum_location.name, player),
rule.simplify())
counter += 1
def set_museum_milestone_rule(logic: StardewLogic, multi_world: MultiWorld, museum_milestone, museum_prefix: str,
def set_museum_milestone_rule(logic: StardewLogic, multiworld: MultiWorld, museum_milestone, museum_prefix: str,
player: int):
milestone_name = museum_milestone.name[len(museum_prefix):]
donations_suffix = " Donations"
@ -462,7 +467,7 @@ def set_museum_milestone_rule(logic: StardewLogic, multi_world: MultiWorld, muse
rule = logic.can_donate_museum_item(Artifact.ancient_seed) & logic.received(metal_detector, 4)
if rule is None:
return
MultiWorldRules.set_rule(multi_world.get_location(museum_milestone.name, player), rule.simplify())
MultiWorldRules.set_rule(multiworld.get_location(museum_milestone.name, player), rule.simplify())
def get_museum_item_count_rule(logic: StardewLogic, suffix, milestone_name, accepted_items, donation_func):
@ -473,156 +478,156 @@ def get_museum_item_count_rule(logic: StardewLogic, suffix, milestone_name, acce
return rule
def set_backpack_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options):
if world_options[options.BackpackProgression] != options.BackpackProgression.option_vanilla:
MultiWorldRules.set_rule(multi_world.get_location("Large Pack", player),
def set_backpack_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
if world_options.backpack_progression != BackpackProgression.option_vanilla:
MultiWorldRules.set_rule(multiworld.get_location("Large Pack", player),
logic.can_spend_money(2000).simplify())
MultiWorldRules.set_rule(multi_world.get_location("Deluxe Pack", player),
MultiWorldRules.set_rule(multiworld.get_location("Deluxe Pack", player),
(logic.can_spend_money(10000) & logic.received("Progressive Backpack")).simplify())
if ModNames.big_backpack in world_options[options.Mods]:
MultiWorldRules.set_rule(multi_world.get_location("Premium Pack", player),
if ModNames.big_backpack in world_options.mods:
MultiWorldRules.set_rule(multiworld.get_location("Premium Pack", player),
(logic.can_spend_money(150000) &
logic.received("Progressive Backpack", 2)).simplify())
def set_festival_rules(all_location_names: List[str], logic: StardewLogic, multi_world, player):
def set_festival_rules(all_location_names: List[str], logic: StardewLogic, multiworld, player):
festival_locations = []
festival_locations.extend(locations.locations_by_tag[LocationTags.FESTIVAL])
festival_locations.extend(locations.locations_by_tag[LocationTags.FESTIVAL_HARD])
festival_locations.extend(locations_by_tag[LocationTags.FESTIVAL])
festival_locations.extend(locations_by_tag[LocationTags.FESTIVAL_HARD])
for festival in festival_locations:
if festival.name in all_location_names:
MultiWorldRules.set_rule(multi_world.get_location(festival.name, player),
MultiWorldRules.set_rule(multiworld.get_location(festival.name, player),
logic.festival_rules[festival.name].simplify())
def set_traveling_merchant_rules(logic: StardewLogic, multi_world: MultiWorld, player: int):
def set_traveling_merchant_rules(logic: StardewLogic, multiworld: MultiWorld, player: int):
for day in Weekday.all_days:
item_for_day = f"Traveling Merchant: {day}"
for i in range(1, 4):
location_name = f"Traveling Merchant {day} Item {i}"
MultiWorldRules.set_rule(multi_world.get_location(location_name, player),
MultiWorldRules.set_rule(multiworld.get_location(location_name, player),
logic.received(item_for_day))
def set_arcade_machine_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options):
MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.play_junimo_kart, player),
def set_arcade_machine_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_junimo_kart, player),
logic.received(Wallet.skull_key).simplify())
if world_options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling:
if world_options.arcade_machine_locations != ArcadeMachineLocations.option_full_shuffling:
return
MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.play_junimo_kart, player),
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_junimo_kart, player),
logic.has("Junimo Kart Small Buff").simplify())
MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.reach_junimo_kart_2, player),
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_2, player),
logic.has("Junimo Kart Medium Buff").simplify())
MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.reach_junimo_kart_3, player),
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_3, player),
logic.has("Junimo Kart Big Buff").simplify())
MultiWorldRules.add_rule(multi_world.get_location("Junimo Kart: Sunset Speedway (Victory)", player),
MultiWorldRules.add_rule(multiworld.get_location("Junimo Kart: Sunset Speedway (Victory)", player),
logic.has("Junimo Kart Max Buff").simplify())
MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.play_journey_of_the_prairie_king, player),
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_journey_of_the_prairie_king, player),
logic.has("JotPK Small Buff").simplify())
MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.reach_jotpk_world_2, player),
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_jotpk_world_2, player),
logic.has("JotPK Medium Buff").simplify())
MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.reach_jotpk_world_3, player),
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_jotpk_world_3, player),
logic.has("JotPK Big Buff").simplify())
MultiWorldRules.add_rule(multi_world.get_location("Journey of the Prairie King Victory", player),
MultiWorldRules.add_rule(multiworld.get_location("Journey of the Prairie King Victory", player),
logic.has("JotPK Max Buff").simplify())
def set_friendsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int):
def set_friendsanity_rules(all_location_names: List[str], logic: StardewLogic, multiworld: MultiWorld, player: int):
friend_prefix = "Friendsanity: "
friend_suffix = " <3"
for friend_location in locations.locations_by_tag[LocationTags.FRIENDSANITY]:
if not friend_location.name in all_location_names:
for friend_location in locations_by_tag[LocationTags.FRIENDSANITY]:
if friend_location.name not in all_location_names:
continue
friend_location_without_prefix = friend_location.name[len(friend_prefix):]
friend_location_trimmed = friend_location_without_prefix[:friend_location_without_prefix.index(friend_suffix)]
split_index = friend_location_trimmed.rindex(" ")
friend_name = friend_location_trimmed[:split_index]
num_hearts = int(friend_location_trimmed[split_index + 1:])
MultiWorldRules.set_rule(multi_world.get_location(friend_location.name, player),
MultiWorldRules.set_rule(multiworld.get_location(friend_location.name, player),
logic.can_earn_relationship(friend_name, num_hearts).simplify())
def set_deepwoods_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options: StardewOptions):
if ModNames.deepwoods in world_options[options.Mods]:
MultiWorldRules.add_rule(multi_world.get_location("Breaking Up Deep Woods Gingerbread House", player),
def set_deepwoods_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
if ModNames.deepwoods in world_options.mods:
MultiWorldRules.add_rule(multiworld.get_location("Breaking Up Deep Woods Gingerbread House", player),
logic.has_tool(Tool.axe, "Gold") & deepwoods.can_reach_woods_depth(logic, 50).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Chop Down a Deep Woods Iridium Tree", player),
MultiWorldRules.add_rule(multiworld.get_location("Chop Down a Deep Woods Iridium Tree", player),
logic.has_tool(Tool.axe, "Iridium").simplify())
MultiWorldRules.set_rule(multi_world.get_entrance(DeepWoodsEntrance.use_woods_obelisk, player),
MultiWorldRules.set_rule(multiworld.get_entrance(DeepWoodsEntrance.use_woods_obelisk, player),
logic.received("Woods Obelisk").simplify())
for depth in range(10, 100 + 10, 10):
MultiWorldRules.set_rule(multi_world.get_entrance(move_to_woods_depth(depth), player),
MultiWorldRules.set_rule(multiworld.get_entrance(move_to_woods_depth(depth), player),
deepwoods.can_chop_to_depth(logic, depth).simplify())
def set_magic_spell_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options: StardewOptions):
if ModNames.magic not in world_options[options.Mods]:
def set_magic_spell_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
if ModNames.magic not in world_options.mods:
return
MultiWorldRules.set_rule(multi_world.get_entrance(MagicEntrance.store_to_altar, player),
(logic.has_relationship(NPC.wizard, 3) &
logic.can_reach_region(Region.wizard_tower)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Clear Debris", player),
((logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic"))
& magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Till", player),
(logic.has_tool("Hoe", "Basic") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Water", player),
(logic.has_tool("Watering Can", "Basic") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze All Toil School Locations", player),
(logic.has_tool("Watering Can", "Basic") & logic.has_tool("Hoe", "Basic")
& (logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic"))
& magic.can_use_altar(logic)).simplify())
MultiWorldRules.set_rule(multiworld.get_entrance(MagicEntrance.store_to_altar, player),
(logic.has_relationship(NPC.wizard, 3) &
logic.can_reach_region(Region.wizard_tower)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze: Clear Debris", player),
((logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic"))
& magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze: Till", player),
(logic.has_tool("Hoe", "Basic") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze: Water", player),
(logic.has_tool("Watering Can", "Basic") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze All Toil School Locations", player),
(logic.has_tool("Watering Can", "Basic") & logic.has_tool("Hoe", "Basic")
& (logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic"))
& magic.can_use_altar(logic)).simplify())
# Do I *want* to add boots into logic when you get them even in vanilla without effort? idk
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Evac", player),
(logic.can_mine_perfectly() & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Haste", player),
(logic.has("Coffee") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Heal", player),
(logic.has("Life Elixir") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze All Life School Locations", player),
(logic.has("Coffee") & logic.has("Life Elixir")
& logic.can_mine_perfectly() & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Descend", player),
(logic.can_reach_region(Region.mines) & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Fireball", player),
(logic.has("Fire Quartz") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Frostbite", player),
(logic.can_mine_to_floor(70) & logic.can_fish(85) & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze All Elemental School Locations", player),
(logic.can_reach_region(Region.mines) & logic.has("Fire Quartz")
& logic.can_reach_region(Region.mines_floor_70) & logic.can_fish(85) &
magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Lantern", player),
magic.can_use_altar(logic).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Tendrils", player),
(logic.can_reach_region(Region.farm) & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Shockwave", player),
(logic.has("Earth Crystal") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze All Nature School Locations", player),
(logic.has("Earth Crystal") & logic.can_reach_region("Farm") &
magic.can_use_altar(logic)).simplify()),
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Meteor", player),
(logic.can_reach_region(Region.farm) & logic.has_lived_months(12)
& magic.can_use_altar(logic)).simplify()),
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Lucksteal", player),
(logic.can_reach_region(Region.witch_hut) & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Bloodmana", player),
(logic.can_reach_region(Region.mines_floor_100) & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze All Eldritch School Locations", player),
(logic.can_reach_region(Region.witch_hut) &
logic.can_reach_region(Region.mines_floor_100) &
logic.can_reach_region(Region.farm) & logic.has_lived_months(12) &
magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze Every Magic School Location", player),
(logic.has_tool("Watering Can", "Basic") & logic.has_tool("Hoe", "Basic")
& (logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic")) &
logic.has("Coffee") & logic.has("Life Elixir")
& logic.can_mine_perfectly() & logic.has("Earth Crystal") &
logic.can_reach_region(Region.mines) &
logic.has("Fire Quartz") & logic.can_fish(85) &
logic.can_reach_region(Region.witch_hut) &
logic.can_reach_region(Region.mines_floor_100) &
logic.can_reach_region(Region.farm) & logic.has_lived_months(12) &
magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze: Evac", player),
(logic.can_mine_perfectly() & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze: Haste", player),
(logic.has("Coffee") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze: Heal", player),
(logic.has("Life Elixir") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze All Life School Locations", player),
(logic.has("Coffee") & logic.has("Life Elixir")
& logic.can_mine_perfectly() & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze: Descend", player),
(logic.can_reach_region(Region.mines) & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze: Fireball", player),
(logic.has("Fire Quartz") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze: Frostbite", player),
(logic.can_mine_to_floor(70) & logic.can_fish(85) & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze All Elemental School Locations", player),
(logic.can_reach_region(Region.mines) & logic.has("Fire Quartz")
& logic.can_reach_region(Region.mines_floor_70) & logic.can_fish(85) &
magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze: Lantern", player),
magic.can_use_altar(logic).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze: Tendrils", player),
(logic.can_reach_region(Region.farm) & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze: Shockwave", player),
(logic.has("Earth Crystal") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze All Nature School Locations", player),
(logic.has("Earth Crystal") & logic.can_reach_region("Farm") &
magic.can_use_altar(logic)).simplify()),
MultiWorldRules.add_rule(multiworld.get_location("Analyze: Meteor", player),
(logic.can_reach_region(Region.farm) & logic.has_lived_months(12)
& magic.can_use_altar(logic)).simplify()),
MultiWorldRules.add_rule(multiworld.get_location("Analyze: Lucksteal", player),
(logic.can_reach_region(Region.witch_hut) & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze: Bloodmana", player),
(logic.can_reach_region(Region.mines_floor_100) & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze All Eldritch School Locations", player),
(logic.can_reach_region(Region.witch_hut) &
logic.can_reach_region(Region.mines_floor_100) &
logic.can_reach_region(Region.farm) & logic.has_lived_months(12) &
magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multiworld.get_location("Analyze Every Magic School Location", player),
(logic.has_tool("Watering Can", "Basic") & logic.has_tool("Hoe", "Basic")
& (logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic")) &
logic.has("Coffee") & logic.has("Life Elixir")
& logic.can_mine_perfectly() & logic.has("Earth Crystal") &
logic.can_reach_region(Region.mines) &
logic.has("Fire Quartz") & logic.can_fish(85) &
logic.can_reach_region(Region.witch_hut) &
logic.can_reach_region(Region.mines_floor_100) &
logic.can_reach_region(Region.farm) & logic.has_lived_months(12) &
magic.can_use_altar(logic)).simplify())

View File

@ -1,6 +1,5 @@
from .. import True_
from ..logic import Received, Has, False_, And, Or, StardewLogic
from ..options import default_options, StardewOptions
from ..logic import Received, Has, False_, And, Or
def test_simplify_true_in_and():

View File

@ -1,15 +1,14 @@
import itertools
import unittest
from random import random
from typing import Dict
from BaseClasses import ItemClassification, MultiWorld
from Options import SpecialRange, OptionSet
from Options import SpecialRange
from . import setup_solo_multiworld, SVTestBase
from .. import StardewItem, options, items_by_group, Group
from .. import StardewItem, items_by_group, Group, StardewValleyWorld
from ..locations import locations_by_tag, LocationTags, location_table
from ..options import StardewOption, stardew_valley_option_classes, Mods
from ..strings.goal_names import Goal
from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations
from ..strings.goal_names import Goal as GoalName
from ..strings.season_names import Season
from ..strings.special_order_names import SpecialOrder
from ..strings.tool_names import ToolMaterial, Tool
@ -51,39 +50,41 @@ def get_option_choices(option) -> Dict[str, int]:
class TestGenerateDynamicOptions(SVTestBase):
def test_given_special_range_when_generate_then_basic_checks(self):
for option in stardew_valley_option_classes:
if not issubclass(option, SpecialRange):
options = self.world.options_dataclass.type_hints
for option_name, option in options.items():
if not isinstance(option, SpecialRange):
continue
for value in option.special_range_names:
with self.subTest(f"{option.internal_name}: {value}"):
choices = {option.internal_name: option.special_range_names[value]}
with self.subTest(f"{option_name}: {value}"):
choices = {option_name: option.special_range_names[value]}
multiworld = setup_solo_multiworld(choices)
basic_checks(self, multiworld)
def test_given_choice_when_generate_then_basic_checks(self):
seed = int(random() * pow(10, 18) - 1)
for option in stardew_valley_option_classes:
options = self.world.options_dataclass.type_hints
for option_name, option in options.items():
if not option.options:
continue
for value in option.options:
with self.subTest(f"{option.internal_name}: {value} [Seed: {seed}]"):
world_options = {option.internal_name: option.options[value]}
with self.subTest(f"{option_name}: {value} [Seed: {seed}]"):
world_options = {option_name: option.options[value]}
multiworld = setup_solo_multiworld(world_options, seed)
basic_checks(self, multiworld)
class TestGoal(SVTestBase):
def test_given_goal_when_generate_then_victory_is_in_correct_location(self):
for goal, location in [("community_center", Goal.community_center),
("grandpa_evaluation", Goal.grandpa_evaluation),
("bottom_of_the_mines", Goal.bottom_of_the_mines),
("cryptic_note", Goal.cryptic_note),
("master_angler", Goal.master_angler),
("complete_collection", Goal.complete_museum),
("full_house", Goal.full_house),
("perfection", Goal.perfection)]:
for goal, location in [("community_center", GoalName.community_center),
("grandpa_evaluation", GoalName.grandpa_evaluation),
("bottom_of_the_mines", GoalName.bottom_of_the_mines),
("cryptic_note", GoalName.cryptic_note),
("master_angler", GoalName.master_angler),
("complete_collection", GoalName.complete_museum),
("full_house", GoalName.full_house),
("perfection", GoalName.perfection)]:
with self.subTest(msg=f"Goal: {goal}, Location: {location}"):
world_options = {options.Goal.internal_name: options.Goal.options[goal]}
world_options = {Goal.internal_name: Goal.options[goal]}
multi_world = setup_solo_multiworld(world_options)
victory = multi_world.find_item("Victory", 1)
self.assertEqual(victory.name, location)
@ -91,14 +92,14 @@ class TestGoal(SVTestBase):
class TestSeasonRandomization(SVTestBase):
def test_given_disabled_when_generate_then_all_seasons_are_precollected(self):
world_options = {options.SeasonRandomization.internal_name: options.SeasonRandomization.option_disabled}
world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_disabled}
multi_world = setup_solo_multiworld(world_options)
precollected_items = {item.name for item in multi_world.precollected_items[1]}
self.assertTrue(all([season in precollected_items for season in SEASONS]))
def test_given_randomized_when_generate_then_all_seasons_are_in_the_pool_or_precollected(self):
world_options = {options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized}
world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_randomized}
multi_world = setup_solo_multiworld(world_options)
precollected_items = {item.name for item in multi_world.precollected_items[1]}
items = {item.name for item in multi_world.get_items()} | precollected_items
@ -106,7 +107,7 @@ class TestSeasonRandomization(SVTestBase):
self.assertEqual(len(SEASONS.intersection(precollected_items)), 1)
def test_given_progressive_when_generate_then_3_progressive_seasons_are_in_the_pool(self):
world_options = {options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive}
world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_progressive}
multi_world = setup_solo_multiworld(world_options)
items = [item.name for item in multi_world.get_items()]
@ -115,7 +116,7 @@ class TestSeasonRandomization(SVTestBase):
class TestToolProgression(SVTestBase):
def test_given_vanilla_when_generate_then_no_tool_in_pool(self):
world_options = {options.ToolProgression.internal_name: options.ToolProgression.option_vanilla}
world_options = {ToolProgression.internal_name: ToolProgression.option_vanilla}
multi_world = setup_solo_multiworld(world_options)
items = {item.name for item in multi_world.get_items()}
@ -123,7 +124,7 @@ class TestToolProgression(SVTestBase):
self.assertNotIn(tool, items)
def test_given_progressive_when_generate_then_progressive_tool_of_each_is_in_pool_four_times(self):
world_options = {options.ToolProgression.internal_name: options.ToolProgression.option_progressive}
world_options = {ToolProgression.internal_name: ToolProgression.option_progressive}
multi_world = setup_solo_multiworld(world_options)
items = [item.name for item in multi_world.get_items()]
@ -131,7 +132,7 @@ class TestToolProgression(SVTestBase):
self.assertEqual(items.count("Progressive " + tool), 4)
def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self):
world_options = {options.ToolProgression.internal_name: options.ToolProgression.option_progressive}
world_options = {ToolProgression.internal_name: ToolProgression.option_progressive}
multi_world = setup_solo_multiworld(world_options)
locations = {locations.name for locations in multi_world.get_locations(1)}
@ -148,50 +149,52 @@ class TestToolProgression(SVTestBase):
class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase):
def test_given_special_range_when_generate_exclude_ginger_island(self):
for option in stardew_valley_option_classes:
if not issubclass(option,
SpecialRange) or option.internal_name == options.ExcludeGingerIsland.internal_name:
options = self.world.options_dataclass.type_hints
for option_name, option in options.items():
if not isinstance(option, SpecialRange) or option_name == ExcludeGingerIsland.internal_name:
continue
for value in option.special_range_names:
with self.subTest(f"{option.internal_name}: {value}"):
with self.subTest(f"{option_name}: {value}"):
multiworld = setup_solo_multiworld(
{options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
option.internal_name: option.special_range_names[value]})
{ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
option_name: option.special_range_names[value]})
check_no_ginger_island(self, multiworld)
def test_given_choice_when_generate_exclude_ginger_island(self):
seed = int(random() * pow(10, 18) - 1)
island_option = options.ExcludeGingerIsland
for option in stardew_valley_option_classes:
if not option.options or option.internal_name == island_option.internal_name:
options = self.world.options_dataclass.type_hints
for option_name, option in options.items():
if not option.options or option_name == ExcludeGingerIsland.internal_name:
continue
for value in option.options:
with self.subTest(f"{option.internal_name}: {value} [Seed: {seed}]"):
with self.subTest(f"{option_name}: {value} [Seed: {seed}]"):
multiworld = setup_solo_multiworld(
{island_option.internal_name: island_option.option_true,
option.internal_name: option.options[value]}, seed)
if multiworld.worlds[self.player].options[island_option.internal_name] != island_option.option_true:
{ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
option_name: option.options[value]}, seed)
stardew_world: StardewValleyWorld = multiworld.worlds[self.player]
if stardew_world.options.exclude_ginger_island != ExcludeGingerIsland.option_true:
continue
basic_checks(self, multiworld)
check_no_ginger_island(self, multiworld)
def test_given_island_related_goal_then_override_exclude_ginger_island(self):
island_goals = [value for value in options.Goal.options if value in ["walnut_hunter", "perfection"]]
island_option = options.ExcludeGingerIsland
island_goals = [value for value in Goal.options if value in ["walnut_hunter", "perfection"]]
island_option = ExcludeGingerIsland
for goal in island_goals:
for value in island_option.options:
with self.subTest(f"Goal: {goal}, {island_option.internal_name}: {value}"):
multiworld = setup_solo_multiworld(
{options.Goal.internal_name: options.Goal.options[goal],
{Goal.internal_name: Goal.options[goal],
island_option.internal_name: island_option.options[value]})
self.assertEqual(multiworld.worlds[self.player].options[island_option.internal_name], island_option.option_false)
stardew_world: StardewValleyWorld = multiworld.worlds[self.player]
self.assertEqual(stardew_world.options.exclude_ginger_island, island_option.option_false)
basic_checks(self, multiworld)
class TestTraps(SVTestBase):
def test_given_no_traps_when_generate_then_no_trap_in_pool(self):
world_options = self.allsanity_options_without_mods()
world_options.update({options.TrapItems.internal_name: options.TrapItems.option_no_traps})
world_options.update({TrapItems.internal_name: TrapItems.option_no_traps})
multi_world = setup_solo_multiworld(world_options)
trap_items = [item_data.name for item_data in items_by_group[Group.TRAP]]
@ -202,12 +205,12 @@ class TestTraps(SVTestBase):
self.assertNotIn(item, multiworld_items)
def test_given_traps_when_generate_then_all_traps_in_pool(self):
trap_option = options.TrapItems
trap_option = TrapItems
for value in trap_option.options:
if value == "no_traps":
continue
world_options = self.allsanity_options_with_mods()
world_options.update({options.TrapItems.internal_name: trap_option.options[value]})
world_options.update({TrapItems.internal_name: trap_option.options[value]})
multi_world = setup_solo_multiworld(world_options)
trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups and item_data.mod_name is None]
multiworld_items = [item.name for item in multi_world.get_items()]
@ -218,7 +221,7 @@ class TestTraps(SVTestBase):
class TestSpecialOrders(SVTestBase):
def test_given_disabled_then_no_order_in_pool(self):
world_options = {options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled}
world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled}
multi_world = setup_solo_multiworld(world_options)
locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table}
@ -228,7 +231,7 @@ class TestSpecialOrders(SVTestBase):
self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags)
def test_given_board_only_then_no_qi_order_in_pool(self):
world_options = {options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_only}
world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only}
multi_world = setup_solo_multiworld(world_options)
locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table}
@ -242,8 +245,8 @@ class TestSpecialOrders(SVTestBase):
self.assertIn(board_location.name, locations_in_pool)
def test_given_board_and_qi_then_all_orders_in_pool(self):
world_options = {options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_victories}
world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories}
multi_world = setup_solo_multiworld(world_options)
locations_in_pool = {location.name for location in multi_world.get_locations()}
@ -258,8 +261,8 @@ class TestSpecialOrders(SVTestBase):
self.assertIn(board_location.name, locations_in_pool)
def test_given_board_and_qi_without_arcade_machines_then_lets_play_a_game_not_in_pool(self):
world_options = {options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled}
world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled}
multi_world = setup_solo_multiworld(world_options)
locations_in_pool = {location.name for location in multi_world.get_locations()}

View File

@ -3,7 +3,8 @@ import sys
import unittest
from . import SVTestBase, setup_solo_multiworld
from .. import StardewOptions, options, StardewValleyWorld
from .. import options, StardewValleyWorld, StardewValleyOptions
from ..options import EntranceRandomization, ExcludeGingerIsland
from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag
connections_by_name = {connection.name for connection in vanilla_connections}
@ -37,11 +38,12 @@ class TestEntranceRando(unittest.TestCase):
seed = random.randrange(sys.maxsize)
with self.subTest(flag=flag, msg=f"Seed: {seed}"):
rand = random.Random(seed)
world_options = StardewOptions({options.EntranceRandomization.internal_name: option,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false})
world_options = {EntranceRandomization.internal_name: option,
ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false}
multiworld = setup_solo_multiworld(world_options)
regions_by_name = {region.name: region for region in vanilla_regions}
_, randomized_connections = randomize_connections(rand, world_options, regions_by_name)
_, randomized_connections = randomize_connections(rand, multiworld.worlds[1].options, regions_by_name)
for connection in vanilla_connections:
if flag in connection.flag:
@ -62,11 +64,12 @@ class TestEntranceRando(unittest.TestCase):
with self.subTest(option=option, flag=flag):
seed = random.randrange(sys.maxsize)
rand = random.Random(seed)
world_options = StardewOptions({options.EntranceRandomization.internal_name: option,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true})
world_options = {EntranceRandomization.internal_name: option,
ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true}
multiworld = setup_solo_multiworld(world_options)
regions_by_name = {region.name: region for region in vanilla_regions}
_, randomized_connections = randomize_connections(rand, world_options, regions_by_name)
_, randomized_connections = randomize_connections(rand, multiworld.worlds[1].options, regions_by_name)
for connection in vanilla_connections:
if flag in connection.flag:

View File

@ -5,9 +5,12 @@ from typing import Dict, FrozenSet, Tuple, Any, ClassVar
from BaseClasses import MultiWorld
from test.TestBase import WorldTestBase
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
from .. import StardewValleyWorld, options
from .. import StardewValleyWorld
from ..mods.mod_data import ModNames
from worlds.AutoWorld import call_all
from ..options import Cropsanity, SkillProgression, SpecialOrderLocations, Friendsanity, NumberOfLuckBuffs, SeasonRandomization, ToolProgression, \
ElevatorProgression, Museumsanity, BackpackProgression, BuildingProgression, ArcadeMachineLocations, HelpWantedLocations, Fishsanity, NumberOfMovementBuffs, \
BundleRandomization, BundlePrice, FestivalLocations, FriendsanityHeartSize, ExcludeGingerIsland, TrapItems, Goal, Mods
class SVTestBase(WorldTestBase):
@ -33,48 +36,48 @@ class SVTestBase(WorldTestBase):
def minimal_locations_maximal_items(self):
min_max_options = {
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized,
options.Cropsanity.internal_name: options.Cropsanity.option_shuffled,
options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla,
options.ToolProgression.internal_name: options.ToolProgression.option_vanilla,
options.SkillProgression.internal_name: options.SkillProgression.option_vanilla,
options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla,
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla,
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled,
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled,
options.HelpWantedLocations.internal_name: 0,
options.Fishsanity.internal_name: options.Fishsanity.option_none,
options.Museumsanity.internal_name: options.Museumsanity.option_none,
options.Friendsanity.internal_name: options.Friendsanity.option_none,
options.NumberOfMovementBuffs.internal_name: 12,
options.NumberOfLuckBuffs.internal_name: 12,
SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
Cropsanity.internal_name: Cropsanity.option_shuffled,
BackpackProgression.internal_name: BackpackProgression.option_vanilla,
ToolProgression.internal_name: ToolProgression.option_vanilla,
SkillProgression.internal_name: SkillProgression.option_vanilla,
BuildingProgression.internal_name: BuildingProgression.option_vanilla,
ElevatorProgression.internal_name: ElevatorProgression.option_vanilla,
ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled,
SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled,
HelpWantedLocations.internal_name: 0,
Fishsanity.internal_name: Fishsanity.option_none,
Museumsanity.internal_name: Museumsanity.option_none,
Friendsanity.internal_name: Friendsanity.option_none,
NumberOfMovementBuffs.internal_name: 12,
NumberOfLuckBuffs.internal_name: 12,
}
return min_max_options
def allsanity_options_without_mods(self):
allsanity = {
options.Goal.internal_name: options.Goal.option_perfection,
options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled,
options.BundlePrice.internal_name: options.BundlePrice.option_expensive,
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized,
options.Cropsanity.internal_name: options.Cropsanity.option_shuffled,
options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive,
options.ToolProgression.internal_name: options.ToolProgression.option_progressive,
options.SkillProgression.internal_name: options.SkillProgression.option_progressive,
options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive,
options.FestivalLocations.internal_name: options.FestivalLocations.option_hard,
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive,
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling,
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
options.HelpWantedLocations.internal_name: 56,
options.Fishsanity.internal_name: options.Fishsanity.option_all,
options.Museumsanity.internal_name: options.Museumsanity.option_all,
options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
options.FriendsanityHeartSize.internal_name: 1,
options.NumberOfMovementBuffs.internal_name: 12,
options.NumberOfLuckBuffs.internal_name: 12,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
options.TrapItems.internal_name: options.TrapItems.option_nightmare,
Goal.internal_name: Goal.option_perfection,
BundleRandomization.internal_name: BundleRandomization.option_shuffled,
BundlePrice.internal_name: BundlePrice.option_expensive,
SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
Cropsanity.internal_name: Cropsanity.option_shuffled,
BackpackProgression.internal_name: BackpackProgression.option_progressive,
ToolProgression.internal_name: ToolProgression.option_progressive,
SkillProgression.internal_name: SkillProgression.option_progressive,
BuildingProgression.internal_name: BuildingProgression.option_progressive,
FestivalLocations.internal_name: FestivalLocations.option_hard,
ElevatorProgression.internal_name: ElevatorProgression.option_progressive,
ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling,
SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
HelpWantedLocations.internal_name: 56,
Fishsanity.internal_name: Fishsanity.option_all,
Museumsanity.internal_name: Museumsanity.option_all,
Friendsanity.internal_name: Friendsanity.option_all_with_marriage,
FriendsanityHeartSize.internal_name: 1,
NumberOfMovementBuffs.internal_name: 12,
NumberOfLuckBuffs.internal_name: 12,
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
TrapItems.internal_name: TrapItems.option_nightmare,
}
return allsanity
@ -89,7 +92,7 @@ class SVTestBase(WorldTestBase):
ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator
)
allsanity.update({options.Mods.internal_name: all_mods})
allsanity.update({Mods.internal_name: all_mods})
return allsanity
pre_generated_worlds = {}
@ -110,7 +113,7 @@ def setup_solo_multiworld(test_options=None, seed=None,
multiworld.set_seed(seed)
# print(f"Seed: {multiworld.seed}") # Uncomment to print the seed for every test
args = Namespace()
for name, option in StardewValleyWorld.option_definitions.items():
for name, option in StardewValleyWorld.options_dataclass.type_hints.items():
value = option(test_options[name]) if name in test_options else option.from_any(option.default)
setattr(args, name, {1: value})
multiworld.set_options(args)

View File

@ -1,11 +1,11 @@
from BaseClasses import MultiWorld
from .option_checks import is_setting, assert_is_setting
from .option_checks import get_stardew_options
from ... import options
from .. import SVTestBase
def is_goal(multiworld: MultiWorld, goal: int) -> bool:
return is_setting(multiworld, options.Goal.internal_name, goal)
return get_stardew_options(multiworld).goal.value == goal
def is_bottom_mines(multiworld: MultiWorld) -> bool:
@ -33,7 +33,7 @@ def is_not_perfection(multiworld: MultiWorld) -> bool:
def assert_ginger_island_is_included(tester: SVTestBase, multiworld: MultiWorld):
assert_is_setting(tester, multiworld, options.ExcludeGingerIsland.internal_name, options.ExcludeGingerIsland.option_false)
tester.assertEqual(get_stardew_options(multiworld).exclude_ginger_island, options.ExcludeGingerIsland.option_false)
def assert_walnut_hunter_world_is_valid(tester: SVTestBase, multiworld: MultiWorld):

View File

@ -1,5 +1,3 @@
from typing import Union
from BaseClasses import MultiWorld
from .world_checks import get_all_item_names, get_all_location_names
from .. import SVTestBase
@ -8,32 +6,16 @@ from ...locations import LocationTags
from ...strings.ap_names.transport_names import Transportation
def get_stardew_world(multiworld: MultiWorld) -> Union[StardewValleyWorld, None]:
def get_stardew_world(multiworld: MultiWorld) -> StardewValleyWorld:
for world_key in multiworld.worlds:
world = multiworld.worlds[world_key]
if isinstance(world, StardewValleyWorld):
return world
return None
raise ValueError("no stardew world in this multiworld")
def is_setting(multiworld: MultiWorld, setting_name: str, setting_value: int) -> bool:
stardew_world = get_stardew_world(multiworld)
if not stardew_world:
return False
current_value = stardew_world.options[setting_name]
return current_value == setting_value
def is_not_setting(multiworld: MultiWorld, setting_name: str, setting_value: int) -> bool:
return not is_setting(multiworld, setting_name, setting_value)
def assert_is_setting(tester: SVTestBase, multiworld: MultiWorld, setting_name: str, setting_value: int) -> bool:
stardew_world = get_stardew_world(multiworld)
if not stardew_world:
return False
current_value = stardew_world.options[setting_name]
tester.assertEqual(current_value, setting_value)
def get_stardew_options(multiworld: MultiWorld) -> options.StardewValleyOptions:
return get_stardew_world(multiworld).options
def assert_can_reach_island(tester: SVTestBase, multiworld: MultiWorld):
@ -49,7 +31,8 @@ def assert_cannot_reach_island(tester: SVTestBase, multiworld: MultiWorld):
def assert_can_reach_island_if_should(tester: SVTestBase, multiworld: MultiWorld):
include_island = is_setting(multiworld, options.ExcludeGingerIsland.internal_name, options.ExcludeGingerIsland.option_false)
stardew_options = get_stardew_options(multiworld)
include_island = stardew_options.exclude_ginger_island.value == options.ExcludeGingerIsland.option_false
if include_island:
assert_can_reach_island(tester, multiworld)
else:
@ -57,7 +40,7 @@ def assert_can_reach_island_if_should(tester: SVTestBase, multiworld: MultiWorld
def assert_cropsanity_same_number_items_and_locations(tester: SVTestBase, multiworld: MultiWorld):
is_cropsanity = is_setting(multiworld, options.Cropsanity.internal_name, options.Cropsanity.option_shuffled)
is_cropsanity = get_stardew_options(multiworld).cropsanity.value == options.Cropsanity.option_shuffled
if not is_cropsanity:
return
@ -80,11 +63,10 @@ def assert_has_deluxe_scarecrow_recipe(tester: SVTestBase, multiworld: MultiWorl
def assert_festivals_give_access_to_deluxe_scarecrow(tester: SVTestBase, multiworld: MultiWorld):
has_festivals = is_not_setting(multiworld, options.FestivalLocations.internal_name, options.FestivalLocations.option_disabled)
stardew_options = get_stardew_options(multiworld)
has_festivals = stardew_options.festival_locations.value != options.FestivalLocations.option_disabled
if not has_festivals:
return
assert_all_rarecrows_exist(tester, multiworld)
assert_has_deluxe_scarecrow_recipe(tester, multiworld)

View File

@ -0,0 +1,63 @@
from typing import List, Union
from BaseClasses import MultiWorld
from worlds.stardew_valley.mods.mod_data import ModNames
from worlds.stardew_valley.test import setup_solo_multiworld
from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestBase
from worlds.stardew_valley.items import item_table
from worlds.stardew_valley.locations import location_table
from worlds.stardew_valley.options import Mods
from .option_names import options_to_include
all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator})
def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld):
if isinstance(chosen_mods, str):
chosen_mods = [chosen_mods]
for multiworld_item in multiworld.get_items():
item = item_table[multiworld_item.name]
tester.assertTrue(item.mod_name is None or item.mod_name in chosen_mods)
for multiworld_location in multiworld.get_locations():
if multiworld_location.event:
continue
location = location_table[multiworld_location.name]
tester.assertTrue(location.mod_name is None or location.mod_name in chosen_mods)
class TestGenerateModsOptions(SVTestBase):
def test_given_mod_pairs_when_generate_then_basic_checks(self):
if self.skip_long_tests:
return
mods = list(all_mods)
num_mods = len(mods)
for mod1_index in range(0, num_mods):
for mod2_index in range(mod1_index + 1, num_mods):
mod1 = mods[mod1_index]
mod2 = mods[mod2_index]
mod_pair = (mod1, mod2)
with self.subTest(f"Mods: {mod_pair}"):
multiworld = setup_solo_multiworld({Mods: mod_pair})
basic_checks(self, multiworld)
check_stray_mod_items(list(mod_pair), self, multiworld)
def test_given_mod_names_when_generate_paired_with_other_options_then_basic_checks(self):
if self.skip_long_tests:
return
num_options = len(options_to_include)
for option_index in range(0, num_options):
option = options_to_include[option_index]
if not option.options:
continue
for value in option.options:
for mod in all_mods:
with self.subTest(f"{option.internal_name}: {value}, Mod: {mod}"):
multiworld = setup_solo_multiworld({option.internal_name: option.options[value], Mods: mod})
basic_checks(self, multiworld)
check_stray_mod_items(mod, self, multiworld)

View File

@ -24,7 +24,6 @@ class TestGenerateDynamicOptions(SVTestBase):
def test_given_option_pair_when_generate_then_basic_checks(self):
if self.skip_long_tests:
return
num_options = len(options_to_include)
for option1_index in range(0, num_options):
for option2_index in range(option1_index + 1, num_options):

View File

@ -1,4 +1,4 @@
from typing import Dict, List
from typing import Dict
import random
from BaseClasses import MultiWorld
@ -9,7 +9,6 @@ from ..checks.goal_checks import assert_perfection_world_is_valid, assert_goal_w
from ..checks.option_checks import assert_can_reach_island_if_should, assert_cropsanity_same_number_items_and_locations, \
assert_festivals_give_access_to_deluxe_scarecrow
from ..checks.world_checks import assert_same_number_items_locations, assert_victory_exists
from ... import options
def get_option_choices(option) -> Dict[str, int]:

View File

@ -1,7 +1,8 @@
from worlds.stardew_valley.options import stardew_valley_option_classes
from ... import StardewValleyWorld
options_to_exclude = ["profit_margin", "starting_money", "multiple_day_sleep_enabled", "multiple_day_sleep_cost",
"experience_multiplier", "friendship_multiplier", "debris_multiplier",
"quick_start", "gifting", "gift_tax"]
options_to_include = [option_to_include for option_to_include in stardew_valley_option_classes
if option_to_include.internal_name not in options_to_exclude]
"quick_start", "gifting", "gift_tax", "progression_balancing", "accessibility", "start_inventory", "start_hints", "death_link"]
options_to_include = [option for option_name, option in StardewValleyWorld.options_dataclass.type_hints.items()
if option_name not in options_to_exclude]

View File

@ -4,21 +4,21 @@ import random
import sys
from BaseClasses import MultiWorld
from worlds.stardew_valley.test import setup_solo_multiworld
from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestBase
from worlds.stardew_valley import options, locations, items, Group, ItemClassification, StardewOptions
from worlds.stardew_valley.mods.mod_data import ModNames
from worlds.stardew_valley.regions import RandomizationFlag, create_final_connections, randomize_connections, create_final_regions
from worlds.stardew_valley.items import item_table, items_by_group
from worlds.stardew_valley.locations import location_table, LocationTags
from worlds.stardew_valley.options import stardew_valley_option_classes, Mods, EntranceRandomization
from ...mods.mod_data import ModNames
from .. import setup_solo_multiworld
from ..TestOptions import basic_checks, SVTestBase
from ... import items, Group, ItemClassification
from ...regions import RandomizationFlag, create_final_connections, randomize_connections, create_final_regions
from ...items import item_table, items_by_group
from ...locations import location_table
from ...options import Mods, EntranceRandomization, Friendsanity, SeasonRandomization, SpecialOrderLocations, ExcludeGingerIsland, TrapItems
mod_list = ["DeepWoods", "Tractor Mod", "Bigger Backpack",
"Luck Skill", "Magic", "Socializing Skill", "Archaeology",
"Cooking Skill", "Binning Skill", "Juna - Roommate NPC",
"Professor Jasper Thomas", "Alec Revisited", "Custom NPC - Yoba", "Custom NPC Eugene",
"'Prophet' Wellwick", "Mister Ginger (cat npc)", "Shiko - New Custom NPC", "Delores - Custom NPC",
"Ayeisha - The Postal Worker (Custom NPC)", "Custom NPC - Riley", "Skull Cavern Elevator"]
all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator})
def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld):
@ -37,54 +37,27 @@ def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase
class TestGenerateModsOptions(SVTestBase):
def test_given_single_mods_when_generate_then_basic_checks(self):
for mod in mod_list:
for mod in all_mods:
with self.subTest(f"Mod: {mod}"):
multi_world = setup_solo_multiworld({Mods: mod})
basic_checks(self, multi_world)
check_stray_mod_items(mod, self, multi_world)
def test_given_mod_pairs_when_generate_then_basic_checks(self):
if self.skip_long_tests:
return
num_mods = len(mod_list)
for mod1_index in range(0, num_mods):
for mod2_index in range(mod1_index + 1, num_mods):
mod1 = mod_list[mod1_index]
mod2 = mod_list[mod2_index]
mods = (mod1, mod2)
with self.subTest(f"Mods: {mods}"):
multiworld = setup_solo_multiworld({Mods: mods})
basic_checks(self, multiworld)
check_stray_mod_items(list(mods), self, multiworld)
def test_given_mod_names_when_generate_paired_with_entrance_randomizer_then_basic_checks(self):
for option in EntranceRandomization.options:
for mod in mod_list:
for mod in all_mods:
with self.subTest(f"entrance_randomization: {option}, Mod: {mod}"):
multiworld = setup_solo_multiworld({EntranceRandomization.internal_name: option, Mods: mod})
basic_checks(self, multiworld)
check_stray_mod_items(mod, self, multiworld)
def test_given_mod_names_when_generate_paired_with_other_options_then_basic_checks(self):
if self.skip_long_tests:
return
for option in stardew_valley_option_classes:
if not option.options:
continue
for value in option.options:
for mod in mod_list:
with self.subTest(f"{option.internal_name}: {value}, Mod: {mod}"):
multiworld = setup_solo_multiworld({option.internal_name: option.options[value], Mods: mod})
basic_checks(self, multiworld)
check_stray_mod_items(mod, self, multiworld)
class TestBaseItemGeneration(SVTestBase):
options = {
options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive,
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
options.Mods.internal_name: mod_list
Friendsanity.internal_name: Friendsanity.option_all_with_marriage,
SeasonRandomization.internal_name: SeasonRandomization.option_progressive,
SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
Mods.internal_name: all_mods
}
def test_all_progression_items_are_added_to_the_pool(self):
@ -105,10 +78,10 @@ class TestBaseItemGeneration(SVTestBase):
class TestNoGingerIslandModItemGeneration(SVTestBase):
options = {
options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
options.Mods.internal_name: mod_list
Friendsanity.internal_name: Friendsanity.option_all_with_marriage,
SeasonRandomization.internal_name: SeasonRandomization.option_progressive,
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
Mods.internal_name: all_mods
}
def test_all_progression_items_except_island_are_added_to_the_pool(self):
@ -134,29 +107,31 @@ class TestModEntranceRando(unittest.TestCase):
def test_mod_entrance_randomization(self):
for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
(options.EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION),
(options.EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]:
for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
(EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION),
(EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]:
with self.subTest(option=option, flag=flag):
seed = random.randrange(sys.maxsize)
rand = random.Random(seed)
world_options = StardewOptions({options.EntranceRandomization.internal_name: option,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
options.Mods.internal_name: mod_list})
final_regions = create_final_regions(world_options)
final_connections = create_final_connections(world_options)
world_options = {EntranceRandomization.internal_name: option,
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
Mods.internal_name: all_mods}
multiworld = setup_solo_multiworld(world_options)
world = multiworld.worlds[1]
final_regions = create_final_regions(world.options)
final_connections = create_final_connections(world.options)
regions_by_name = {region.name: region for region in final_regions}
_, randomized_connections = randomize_connections(rand, world_options, regions_by_name)
_, randomized_connections = randomize_connections(rand, world.options, regions_by_name)
for connection in final_connections:
if flag in connection.flag:
connection_in_randomized = connection.name in randomized_connections
reverse_in_randomized = connection.reverse in randomized_connections
self.assertTrue(connection_in_randomized,
f"Connection {connection.name} should be randomized but it is not in the output. Seed = {seed}")
f"Connection {connection.name} should be randomized but it is not in the output. Seed = {seed}")
self.assertTrue(reverse_in_randomized,
f"Connection {connection.reverse} should be randomized but it is not in the output. Seed = {seed}")
f"Connection {connection.reverse} should be randomized but it is not in the output. Seed = {seed}")
self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()),
f"Connections are duplicated in randomization. Seed = {seed}")
@ -164,12 +139,11 @@ class TestModEntranceRando(unittest.TestCase):
class TestModTraps(SVTestBase):
def test_given_traps_when_generate_then_all_traps_in_pool(self):
trap_option = options.TrapItems
for value in trap_option.options:
for value in TrapItems.options:
if value == "no_traps":
continue
world_options = self.allsanity_options_without_mods()
world_options.update({options.TrapItems.internal_name: trap_option.options[value], Mods: "Magic"})
world_options.update({TrapItems.internal_name: TrapItems.options[value], Mods: "Magic"})
multi_world = setup_solo_multiworld(world_options)
trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups]
multiworld_items = [item.name for item in multi_world.get_items()]

View File

@ -12,7 +12,7 @@ from BaseClasses import ItemClassification, LocationProgressType, \
MultiWorld, Item, CollectionState, Entrance, Tutorial
from .logic import cs_to_zz_locs
from .region import ZillionLocation, ZillionRegion
from .options import ZillionStartChar, zillion_options, validate
from .options import ZillionOptions, ZillionStartChar, validate
from .id_maps import item_name_to_id as _item_name_to_id, \
loc_name_to_id as _loc_name_to_id, make_id_to_others, \
zz_reg_name_to_reg_name, base_id
@ -70,7 +70,9 @@ class ZillionWorld(World):
game = "Zillion"
web = ZillionWebWorld()
option_definitions = zillion_options
options_dataclass = ZillionOptions
options: ZillionOptions
settings: typing.ClassVar[ZillionSettings]
topology_present = True # indicate if world type has any meaningful layout/pathing
@ -142,7 +144,10 @@ class ZillionWorld(World):
if not hasattr(self.multiworld, "zillion_logic_cache"):
setattr(self.multiworld, "zillion_logic_cache", {})
zz_op, item_counts = validate(self.multiworld, self.player)
zz_op, item_counts = validate(self.options)
if zz_op.early_scope:
self.multiworld.early_items[self.player]["Scope"] = 1
self._item_counts = item_counts
@ -299,7 +304,8 @@ class ZillionWorld(World):
elif start_char_counts["Champ"] > start_char_counts["Apple"]:
to_stay = "Champ"
else: # equal
to_stay = multiworld.random.choice(("Apple", "Champ"))
choices: Tuple[Literal['Apple', 'Champ', 'JJ'], ...] = ("Apple", "Champ")
to_stay = multiworld.random.choice(choices)
for p, sc in players_start_chars:
if sc != to_stay:

View File

@ -1,13 +1,14 @@
from collections import Counter
# import logging
from typing import TYPE_CHECKING, Any, Dict, Tuple, cast
from Options import AssembleOptions, DefaultOnToggle, Range, SpecialRange, Toggle, Choice
from dataclasses import dataclass
from typing import Dict, Tuple
from typing_extensions import TypeGuard # remove when Python >= 3.10
from Options import DefaultOnToggle, PerGameCommonOptions, Range, SpecialRange, Toggle, Choice
from zilliandomizer.options import \
Options as ZzOptions, char_to_gun, char_to_jump, ID, \
VBLR as ZzVBLR, chars, Chars, ItemCounts as ZzItemCounts
from zilliandomizer.options.parsing import validate as zz_validate
if TYPE_CHECKING:
from BaseClasses import MultiWorld
class ZillionContinues(SpecialRange):
@ -41,6 +42,19 @@ class VBLR(Choice):
option_restrictive = 3
default = 1
def to_zz_vblr(self) -> ZzVBLR:
def is_vblr(o: str) -> TypeGuard[ZzVBLR]:
"""
This function is because mypy doesn't support narrowing with `in`,
https://github.com/python/mypy/issues/12535
so this is the only way I see to get type narrowing to `Literal`.
"""
return o in ("vanilla", "balanced", "low", "restrictive")
key = self.current_key
assert is_vblr(key), f"{key=}"
return key
class ZillionGunLevels(VBLR):
"""
@ -225,27 +239,27 @@ class ZillionRoomGen(Toggle):
display_name = "room generation"
zillion_options: Dict[str, AssembleOptions] = {
"continues": ZillionContinues,
"floppy_req": ZillionFloppyReq,
"gun_levels": ZillionGunLevels,
"jump_levels": ZillionJumpLevels,
"randomize_alarms": ZillionRandomizeAlarms,
"max_level": ZillionMaxLevel,
"start_char": ZillionStartChar,
"opas_per_level": ZillionOpasPerLevel,
"id_card_count": ZillionIDCardCount,
"bread_count": ZillionBreadCount,
"opa_opa_count": ZillionOpaOpaCount,
"zillion_count": ZillionZillionCount,
"floppy_disk_count": ZillionFloppyDiskCount,
"scope_count": ZillionScopeCount,
"red_id_card_count": ZillionRedIDCardCount,
"early_scope": ZillionEarlyScope,
"skill": ZillionSkill,
"starting_cards": ZillionStartingCards,
"room_gen": ZillionRoomGen,
}
@dataclass
class ZillionOptions(PerGameCommonOptions):
continues: ZillionContinues
floppy_req: ZillionFloppyReq
gun_levels: ZillionGunLevels
jump_levels: ZillionJumpLevels
randomize_alarms: ZillionRandomizeAlarms
max_level: ZillionMaxLevel
start_char: ZillionStartChar
opas_per_level: ZillionOpasPerLevel
id_card_count: ZillionIDCardCount
bread_count: ZillionBreadCount
opa_opa_count: ZillionOpaOpaCount
zillion_count: ZillionZillionCount
floppy_disk_count: ZillionFloppyDiskCount
scope_count: ZillionScopeCount
red_id_card_count: ZillionRedIDCardCount
early_scope: ZillionEarlyScope
skill: ZillionSkill
starting_cards: ZillionStartingCards
room_gen: ZillionRoomGen
def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts:
@ -262,47 +276,34 @@ def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts:
return tr
def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]":
def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]":
"""
adjusts options to make game completion possible
`world` parameter is MultiWorld object that has my options on it
`p` is my player id
`options` parameter is ZillionOptions object that was put on my world by the core
"""
for option_name in zillion_options:
assert hasattr(world, option_name), f"Zillion option {option_name} didn't get put in world object"
wo = cast(Any, world) # so I don't need getattr on all the options
skill = wo.skill[p].value
skill = options.skill.value
jump_levels = cast(ZillionJumpLevels, wo.jump_levels[p])
jump_option = jump_levels.current_key
required_level = char_to_jump["Apple"][cast(ZzVBLR, jump_option)].index(3) + 1
jump_option = options.jump_levels.to_zz_vblr()
required_level = char_to_jump["Apple"][jump_option].index(3) + 1
if skill == 0:
# because of hp logic on final boss
required_level = 8
gun_levels = cast(ZillionGunLevels, wo.gun_levels[p])
gun_option = gun_levels.current_key
guns_required = char_to_gun["Champ"][cast(ZzVBLR, gun_option)].index(3)
gun_option = options.gun_levels.to_zz_vblr()
guns_required = char_to_gun["Champ"][gun_option].index(3)
floppy_req = cast(ZillionFloppyReq, wo.floppy_req[p])
floppy_req = options.floppy_req
card = cast(ZillionIDCardCount, wo.id_card_count[p])
bread = cast(ZillionBreadCount, wo.bread_count[p])
opa = cast(ZillionOpaOpaCount, wo.opa_opa_count[p])
gun = cast(ZillionZillionCount, wo.zillion_count[p])
floppy = cast(ZillionFloppyDiskCount, wo.floppy_disk_count[p])
scope = cast(ZillionScopeCount, wo.scope_count[p])
red = cast(ZillionRedIDCardCount, wo.red_id_card_count[p])
item_counts = Counter({
"ID Card": card,
"Bread": bread,
"Opa-Opa": opa,
"Zillion": gun,
"Floppy Disk": floppy,
"Scope": scope,
"Red ID Card": red
"ID Card": options.id_card_count,
"Bread": options.bread_count,
"Opa-Opa": options.opa_opa_count,
"Zillion": options.zillion_count,
"Floppy Disk": options.floppy_disk_count,
"Scope": options.scope_count,
"Red ID Card": options.red_id_card_count
})
minimums = Counter({
"ID Card": 0,
@ -335,10 +336,10 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]":
item_counts["Empty"] += diff
assert sum(item_counts.values()) == 144
max_level = cast(ZillionMaxLevel, wo.max_level[p])
max_level = options.max_level
max_level.value = max(required_level, max_level.value)
opas_per_level = cast(ZillionOpasPerLevel, wo.opas_per_level[p])
opas_per_level = options.opas_per_level
while (opas_per_level.value > 1) and (1 + item_counts["Opa-Opa"] // opas_per_level.value < max_level.value):
# logging.warning(
# "zillion options validate: option opas_per_level incompatible with options max_level and opa_opa_count"
@ -347,39 +348,34 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]":
# that should be all of the level requirements met
name_capitalization = {
name_capitalization: Dict[str, Chars] = {
"jj": "JJ",
"apple": "Apple",
"champ": "Champ",
}
start_char = cast(ZillionStartChar, wo.start_char[p])
start_char = options.start_char
start_char_name = name_capitalization[start_char.current_key]
assert start_char_name in chars
start_char_name = cast(Chars, start_char_name)
starting_cards = cast(ZillionStartingCards, wo.starting_cards[p])
starting_cards = options.starting_cards
room_gen = cast(ZillionRoomGen, wo.room_gen[p])
early_scope = cast(ZillionEarlyScope, wo.early_scope[p])
if early_scope:
world.early_items[p]["Scope"] = 1
room_gen = options.room_gen
zz_item_counts = convert_item_counts(item_counts)
zz_op = ZzOptions(
zz_item_counts,
cast(ZzVBLR, jump_option),
cast(ZzVBLR, gun_option),
jump_option,
gun_option,
opas_per_level.value,
max_level.value,
False, # tutorial
skill,
start_char_name,
floppy_req.value,
wo.continues[p].value,
wo.randomize_alarms[p].value,
False, # early scope is done with AP early_items API
options.continues.value,
bool(options.randomize_alarms.value),
bool(options.early_scope.value),
True, # balance defense
starting_cards.value,
bool(room_gen.value)

View File

@ -1 +1,2 @@
zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@d7122bcbeda40da5db26d60fad06246a1331706f#0.5.4
typing-extensions>=4.7, <5

View File

@ -1,6 +1,6 @@
from . import ZillionTestBase
from worlds.zillion.options import ZillionJumpLevels, ZillionGunLevels, validate
from worlds.zillion.options import ZillionJumpLevels, ZillionGunLevels, ZillionOptions, validate
from zilliandomizer.options import VBLR_CHOICES
@ -9,7 +9,9 @@ class OptionsTest(ZillionTestBase):
def test_validate_default(self) -> None:
self.world_setup()
validate(self.multiworld, 1)
options = self.multiworld.worlds[1].options
assert isinstance(options, ZillionOptions)
validate(options)
def test_vblr_ap_to_zz(self) -> None:
""" all of the valid values for the AP options map to valid values for ZZ options """
@ -20,7 +22,9 @@ class OptionsTest(ZillionTestBase):
for value in vblr_class.name_lookup.values():
self.options = {option_name: value}
self.world_setup()
zz_options, _item_counts = validate(self.multiworld, 1)
options = self.multiworld.worlds[1].options
assert isinstance(options, ZillionOptions)
zz_options, _item_counts = validate(options)
assert getattr(zz_options, option_name) in VBLR_CHOICES
# TODO: test validate with invalid combinations of options