Files
Grinch-AP/worlds/stardew_valley/bundles.py
Aaron Wagener 7193182294 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>
2023-10-10 22:30:20 +02:00

255 lines
13 KiB
Python

from random import Random
from typing import List, Dict, Union
from .data.bundle_data import *
from .logic import StardewLogic
from .options import BundleRandomization, BundlePrice
vanilla_bundles = {
"Pantry/0": "Spring Crops/O 465 20/24 1 0 188 1 0 190 1 0 192 1 0/0",
"Pantry/1": "Summer Crops/O 621 1/256 1 0 260 1 0 258 1 0 254 1 0/3",
"Pantry/2": "Fall Crops/BO 10 1/270 1 0 272 1 0 276 1 0 280 1 0/2",
"Pantry/3": "Quality Crops/BO 15 1/24 5 2 254 5 2 276 5 2 270 5 2/6/3",
"Pantry/4": "Animal/BO 16 1/186 1 0 182 1 0 174 1 0 438 1 0 440 1 0 442 1 0/4/5",
# 639 1 0 640 1 0 641 1 0 642 1 0 643 1 0
"Pantry/5": "Artisan/BO 12 1/432 1 0 428 1 0 426 1 0 424 1 0 340 1 0 344 1 0 613 1 0 634 1 0 635 1 0 636 1 0 637 1 0 638 1 0/1/6",
"Crafts Room/13": "Spring Foraging/O 495 30/16 1 0 18 1 0 20 1 0 22 1 0/0",
"Crafts Room/14": "Summer Foraging/O 496 30/396 1 0 398 1 0 402 1 0/3",
"Crafts Room/15": "Fall Foraging/O 497 30/404 1 0 406 1 0 408 1 0 410 1 0/2",
"Crafts Room/16": "Winter Foraging/O 498 30/412 1 0 414 1 0 416 1 0 418 1 0/6",
"Crafts Room/17": "Construction/BO 114 1/388 99 0 388 99 0 390 99 0 709 10 0/4",
"Crafts Room/19": "Exotic Foraging/O 235 5/88 1 0 90 1 0 78 1 0 420 1 0 422 1 0 724 1 0 725 1 0 726 1 0 257 1 0/1/5",
"Fish Tank/6": "River Fish/O 685 30/145 1 0 143 1 0 706 1 0 699 1 0/6",
"Fish Tank/7": "Lake Fish/O 687 1/136 1 0 142 1 0 700 1 0 698 1 0/0",
"Fish Tank/8": "Ocean Fish/O 690 5/131 1 0 130 1 0 150 1 0 701 1 0/5",
"Fish Tank/9": "Night Fishing/R 516 1/140 1 0 132 1 0 148 1 0/1",
"Fish Tank/10": "Specialty Fish/O 242 5/128 1 0 156 1 0 164 1 0 734 1 0/4",
"Fish Tank/11": "Crab Pot/O 710 3/715 1 0 716 1 0 717 1 0 718 1 0 719 1 0 720 1 0 721 1 0 722 1 0 723 1 0 372 1 0/1/5",
"Boiler Room/20": "Blacksmith's/BO 13 1/334 1 0 335 1 0 336 1 0/2",
"Boiler Room/21": "Geologist's/O 749 5/80 1 0 86 1 0 84 1 0 82 1 0/1",
"Boiler Room/22": "Adventurer's/R 518 1/766 99 0 767 10 0 768 1 0 769 1 0/1/2",
"Vault/23": "2,500g/O 220 3/-1 2500 2500/4",
"Vault/24": "5,000g/O 369 30/-1 5000 5000/2",
"Vault/25": "10,000g/BO 9 1/-1 10000 10000/3",
"Vault/26": "25,000g/BO 21 1/-1 25000 25000/1",
"Bulletin Board/31": "Chef's/O 221 3/724 1 0 259 1 0 430 1 0 376 1 0 228 1 0 194 1 0/4",
"Bulletin Board/32": "Field Research/BO 20 1/422 1 0 392 1 0 702 1 0 536 1 0/5",
"Bulletin Board/33": "Enchanter's/O 336 5/725 1 0 348 1 0 446 1 0 637 1 0/1",
"Bulletin Board/34": "Dye/BO 25 1/420 1 0 397 1 0 421 1 0 444 1 0 62 1 0 266 1 0/6",
"Bulletin Board/35": "Fodder/BO 104 1/262 10 0 178 10 0 613 3 0/3",
# "Abandoned Joja Mart/36": "The Missing//348 1 1 807 1 0 74 1 0 454 5 2 795 1 2 445 1 0/1/5"
}
class Bundle:
room: str
sprite: str
original_name: str
name: str
rewards: List[str]
requirements: List[BundleItem]
color: str
number_required: int
def __init__(self, key: str, value: str):
key_parts = key.split("/")
self.room = key_parts[0]
self.sprite = key_parts[1]
value_parts = value.split("/")
self.original_name = value_parts[0]
self.name = value_parts[0]
self.rewards = self.parse_stardew_objects(value_parts[1])
self.requirements = self.parse_stardew_bundle_items(value_parts[2])
self.color = value_parts[3]
if len(value_parts) > 4:
self.number_required = int(value_parts[4])
else:
self.number_required = len(self.requirements)
def __repr__(self):
return f"{self.original_name} -> {repr(self.requirements)}"
def get_name_with_bundle(self) -> str:
return f"{self.original_name} Bundle"
def to_pair(self) -> (str, str):
key = f"{self.room}/{self.sprite}"
str_rewards = ""
for reward in self.rewards:
str_rewards += f" {reward}"
str_rewards = str_rewards.strip()
str_requirements = ""
for requirement in self.requirements:
str_requirements += f" {requirement.item.item_id} {requirement.amount} {requirement.quality}"
str_requirements = str_requirements.strip()
value = f"{self.name}/{str_rewards}/{str_requirements}/{self.color}/{self.number_required}"
return key, value
def remove_rewards(self):
self.rewards = []
def change_number_required(self, difference: int):
self.number_required = min(len(self.requirements), max(1, self.number_required + difference))
if len(self.requirements) == 1 and self.requirements[0].item.item_id == -1:
one_fifth = self.requirements[0].amount / 5
new_amount = int(self.requirements[0].amount + (difference * one_fifth))
self.requirements[0] = BundleItem.money_bundle(new_amount)
thousand_amount = int(new_amount / 1000)
dollar_amount = str(new_amount % 1000)
while len(dollar_amount) < 3:
dollar_amount = f"0{dollar_amount}"
self.name = f"{thousand_amount},{dollar_amount}g"
def randomize_requirements(self, random: Random,
potential_requirements: Union[List[BundleItem], List[List[BundleItem]]]):
if not potential_requirements:
return
number_to_generate = len(self.requirements)
self.requirements.clear()
if number_to_generate > len(potential_requirements):
choices: Union[BundleItem, List[BundleItem]] = random.choices(potential_requirements, k=number_to_generate)
else:
choices: Union[BundleItem, List[BundleItem]] = random.sample(potential_requirements, number_to_generate)
for choice in choices:
if isinstance(choice, BundleItem):
self.requirements.append(choice)
else:
self.requirements.append(random.choice(choice))
def assign_requirements(self, new_requirements: List[BundleItem]) -> List[BundleItem]:
number_to_generate = len(self.requirements)
self.requirements.clear()
for requirement in new_requirements:
self.requirements.append(requirement)
if len(self.requirements) >= number_to_generate:
return new_requirements[number_to_generate:]
@staticmethod
def parse_stardew_objects(string_objects: str) -> List[str]:
objects = []
if len(string_objects) < 5:
return objects
rewards_parts = string_objects.split(" ")
for index in range(0, len(rewards_parts), 3):
objects.append(f"{rewards_parts[index]} {rewards_parts[index + 1]} {rewards_parts[index + 2]}")
return objects
@staticmethod
def parse_stardew_bundle_items(string_objects: str) -> List[BundleItem]:
bundle_items = []
parts = string_objects.split(" ")
for index in range(0, len(parts), 3):
item_id = int(parts[index])
bundle_item = BundleItem(all_bundle_items_by_id[item_id].item,
int(parts[index + 1]),
int(parts[index + 2]))
bundle_items.append(bundle_item)
return bundle_items
# Shuffling the Vault doesn't really work with the stardew system in place
# shuffle_vault_amongst_themselves(random, bundles)
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]
bundle = Bundle(bundle_key, bundle_value)
bundles[bundle.get_name_with_bundle()] = bundle
if randomization == BundleRandomization.option_thematic:
shuffle_bundles_thematically(random, bundles)
elif randomization == BundleRandomization.option_shuffled:
shuffle_bundles_completely(random, logic, bundles)
price_difference = 0
if price == BundlePrice.option_very_cheap:
price_difference = -2
elif price == BundlePrice.option_cheap:
price_difference = -1
elif price == BundlePrice.option_expensive:
price_difference = 1
for bundle_key in bundles:
bundles[bundle_key].remove_rewards()
bundles[bundle_key].change_number_required(price_difference)
return bundles
def shuffle_bundles_completely(random: Random, logic: StardewLogic, bundles: Dict[str, Bundle]):
total_required_item_number = sum(len(bundle.requirements) for bundle in bundles.values())
quality_crops_items_set = set(quality_crops_items)
all_bundle_items_without_quality_and_money = [item
for item in all_bundle_items_except_money
if item not in quality_crops_items_set] + \
random.sample(quality_crops_items, 10)
choices = random.sample(all_bundle_items_without_quality_and_money, total_required_item_number - 4)
items_sorted = sorted(choices, key=lambda x: logic.item_rules[x.item.name].get_difficulty())
keys = sorted(bundles.keys())
random.shuffle(keys)
for key in keys:
if not bundles[key].original_name.endswith("00g"):
items_sorted = bundles[key].assign_requirements(items_sorted)
def shuffle_bundles_thematically(random: Random, bundles: Dict[str, Bundle]):
shuffle_crafts_room_bundle_thematically(random, bundles)
shuffle_pantry_bundle_thematically(random, bundles)
shuffle_fish_tank_thematically(random, bundles)
shuffle_boiler_room_thematically(random, bundles)
shuffle_bulletin_board_thematically(random, bundles)
def shuffle_crafts_room_bundle_thematically(random: Random, bundles: Dict[str, Bundle]):
bundles["Spring Foraging Bundle"].randomize_requirements(random, spring_foraging_items)
bundles["Summer Foraging Bundle"].randomize_requirements(random, summer_foraging_items)
bundles["Fall Foraging Bundle"].randomize_requirements(random, fall_foraging_items)
bundles["Winter Foraging Bundle"].randomize_requirements(random, winter_foraging_items)
bundles["Exotic Foraging Bundle"].randomize_requirements(random, exotic_foraging_items)
bundles["Construction Bundle"].randomize_requirements(random, construction_items)
def shuffle_pantry_bundle_thematically(random: Random, bundles: Dict[str, Bundle]):
bundles["Spring Crops Bundle"].randomize_requirements(random, spring_crop_items)
bundles["Summer Crops Bundle"].randomize_requirements(random, summer_crops_items)
bundles["Fall Crops Bundle"].randomize_requirements(random, fall_crops_items)
bundles["Quality Crops Bundle"].randomize_requirements(random, quality_crops_items)
bundles["Animal Bundle"].randomize_requirements(random, animal_product_items)
bundles["Artisan Bundle"].randomize_requirements(random, artisan_goods_items)
def shuffle_fish_tank_thematically(random: Random, bundles: Dict[str, Bundle]):
bundles["River Fish Bundle"].randomize_requirements(random, river_fish_items)
bundles["Lake Fish Bundle"].randomize_requirements(random, lake_fish_items)
bundles["Ocean Fish Bundle"].randomize_requirements(random, ocean_fish_items)
bundles["Night Fishing Bundle"].randomize_requirements(random, night_fish_items)
bundles["Crab Pot Bundle"].randomize_requirements(random, crab_pot_items)
bundles["Specialty Fish Bundle"].randomize_requirements(random, specialty_fish_items)
def shuffle_boiler_room_thematically(random: Random, bundles: Dict[str, Bundle]):
bundles["Blacksmith's Bundle"].randomize_requirements(random, blacksmith_items)
bundles["Geologist's Bundle"].randomize_requirements(random, geologist_items)
bundles["Adventurer's Bundle"].randomize_requirements(random, adventurer_items)
def shuffle_bulletin_board_thematically(random: Random, bundles: Dict[str, Bundle]):
bundles["Chef's Bundle"].randomize_requirements(random, chef_items)
bundles["Dye Bundle"].randomize_requirements(random, dye_items)
bundles["Field Research Bundle"].randomize_requirements(random, field_research_items)
bundles["Fodder Bundle"].randomize_requirements(random, fodder_items)
bundles["Enchanter's Bundle"].randomize_requirements(random, enchanter_items)
def shuffle_vault_amongst_themselves(random: Random, bundles: Dict[str, Bundle]):
bundles["2,500g Bundle"].randomize_requirements(random, vault_bundle_items)
bundles["5,000g Bundle"].randomize_requirements(random, vault_bundle_items)
bundles["10,000g Bundle"].randomize_requirements(random, vault_bundle_items)
bundles["25,000g Bundle"].randomize_requirements(random, vault_bundle_items)