Stardew Valley: 4.x.x - The Ginger Update (#1931)

## What is this fixing or adding?
Major content update for Stardew Valley

## How was this tested?
One large-scale public Beta on the archipelago server, plus several smaller private asyncs and test runs

You can go to https://github.com/agilbert1412/StardewArchipelago/releases to grab the mod (latest 4.x.x version), the supported mods and the apworld, to test this PR

## New Features:
- Festival Checks [Easy mode or Hard Mode]
- Special Orders [Both Board and Qi]
- Willy's Boat
- Ginger Island Parrots
- TV Channels
- Trap Items [Available in various difficulty levels]
- Entrance Randomizer: Buildings and Chaos
- New Fishsanity options: Exclude Legendaries, Exclude Hard fish, Only easy fish
- Resource Pack overhaul [Resource packs are now more enjoyable and varied]
- Goal: Greatest Walnut Hunter [Find every single Golden Walnut]
- Goal: Perfection [Achieve Perfection]
- Option: Profit Margin [Multiplier over all earnings]
- Option: Friendsanity Heart Size [Reduce clutter from friendsanity hearts]
- Option: Exclude Ginger Island - will exclude many locations and items to generate a playthrough that does not go to the island
- Mod Support [Curated list of mods]

## New Contributors:
@Witchybun for the mod support

---------

Co-authored-by: Witchybun <embenham05@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
This commit is contained in:
agilbert1412
2023-07-19 14:26:38 -04:00
committed by GitHub
parent 1f6db12797
commit 62657df3fb
94 changed files with 8320 additions and 3104 deletions

View File

@@ -1,28 +1,52 @@
import itertools
import unittest
from random import random
from typing import Dict
from BaseClasses import ItemClassification, MultiWorld
from Options import SpecialRange
from Options import SpecialRange, OptionSet
from . import setup_solo_multiworld, SVTestBase
from .. import StardewItem, options
from ..options import StardewOption, stardew_valley_option_classes
from .. import StardewItem, options, items_by_group, Group
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 ..strings.season_names import Season
from ..strings.special_order_names import SpecialOrder
from ..strings.tool_names import ToolMaterial, Tool
SEASONS = {"Spring", "Summer", "Fall", "Winter"}
SEASONS = {Season.spring, Season.summer, Season.fall, Season.winter}
TOOLS = {"Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can", "Fishing Rod"}
def assert_can_win(multiworld: MultiWorld):
def assert_can_win(tester: SVTestBase, multiworld: MultiWorld):
for item in multiworld.get_items():
multiworld.state.collect(item)
assert multiworld.find_item("Victory", 1).can_reach(multiworld.state)
tester.assertTrue(multiworld.find_item("Victory", 1).can_reach(multiworld.state))
def basic_checks(multiworld: MultiWorld):
assert StardewItem("Victory", ItemClassification.progression, None, 1) in multiworld.get_items()
assert_can_win(multiworld)
assert len(multiworld.itempool) == len(
[location for location in multiworld.get_locations() if not location.event])
def basic_checks(tester: SVTestBase, multiworld: MultiWorld):
tester.assertIn(StardewItem("Victory", ItemClassification.progression, None, 1), multiworld.get_items())
assert_can_win(tester, multiworld)
non_event_locations = [location for location in multiworld.get_locations() if not location.event]
tester.assertEqual(len(multiworld.itempool), len(non_event_locations))
def check_no_ginger_island(tester: SVTestBase, multiworld: MultiWorld):
ginger_island_items = [item_data.name for item_data in items_by_group[Group.GINGER_ISLAND]]
ginger_island_locations = [location_data.name for location_data in locations_by_tag[LocationTags.GINGER_ISLAND]]
for item in multiworld.get_items():
tester.assertNotIn(item.name, ginger_island_items)
for location in multiworld.get_locations():
tester.assertNotIn(location.name, ginger_island_locations)
def get_option_choices(option) -> Dict[str, int]:
if issubclass(option, SpecialRange):
return option.special_range_names
elif option.options:
return option.options
return {}
class TestGenerateDynamicOptions(SVTestBase):
@@ -30,45 +54,39 @@ class TestGenerateDynamicOptions(SVTestBase):
for option in stardew_valley_option_classes:
if not issubclass(option, SpecialRange):
continue
with self.subTest(msg=option.internal_name):
for value in option.special_range_names:
multiworld = setup_solo_multiworld({option.internal_name: option.special_range_names[value]})
basic_checks(multiworld)
for value in option.special_range_names:
with self.subTest(f"{option.internal_name}: {value}"):
choices = {option.internal_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:
if not option.options:
continue
with self.subTest(msg=option.internal_name):
for value in option.options:
multiworld = setup_solo_multiworld({option.internal_name: option.options[value]})
basic_checks(multiworld)
def test_given_option_combination_when_generate_then_basic_checks(self):
option_combinations = [{options.Goal.internal_name: options.Goal.option_master_angler,
options.ToolProgression.internal_name: options.ToolProgression.option_vanilla}]
ids = ["Master Angler + Vanilla tools"]
for i in range(0, len(option_combinations)):
option_combination = option_combinations[i]
id = ids[i]
with self.subTest(msg=f"{id}"):
multi_world = setup_solo_multiworld(option_combination)
basic_checks(multi_world)
for value in option.options:
with self.subTest(f"{option.internal_name}: {value} [Seed: {seed}]"):
choices = {option.internal_name: option.options[value]}
multiworld = setup_solo_multiworld(choices, 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", "Complete Community Center"),
("grandpa_evaluation", "Succeed Grandpa's Evaluation"),
("bottom_of_the_mines", "Reach the Bottom of The Mines"),
("cryptic_note", "Complete Quest Cryptic Note"),
("master_angler", "Catch Every Fish")]:
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)]:
with self.subTest(msg=f"Goal: {goal}, Location: {location}"):
world_options = {options.Goal.internal_name: options.Goal.options[goal]}
multi_world = setup_solo_multiworld(world_options)
victory = multi_world.find_item("Victory", 1)
assert victory.name == location
self.assertEqual(victory.name, location)
class TestSeasonRandomization(SVTestBase):
@@ -77,22 +95,22 @@ class TestSeasonRandomization(SVTestBase):
multi_world = setup_solo_multiworld(world_options)
precollected_items = {item.name for item in multi_world.precollected_items[1]}
assert all([season in precollected_items for season in SEASONS])
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}
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
assert all([season in items for season in SEASONS])
assert len(SEASONS.intersection(precollected_items)) == 1
self.assertTrue(all([season in items for season in SEASONS]))
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}
multi_world = setup_solo_multiworld(world_options)
items = [item.name for item in multi_world.get_items()]
assert items.count("Progressive Season") == 3
self.assertEqual(items.count(Season.progressive), 3)
class TestBackpackProgression(SVTestBase):
@@ -106,21 +124,22 @@ class TestBackpackProgression(SVTestBase):
world_options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive}
multi_world = setup_solo_multiworld(world_options)
items = [item.name for item in multi_world.get_items()]
assert items.count("Progressive Backpack") == 2
self.assertEqual(items.count("Progressive Backpack"), 2)
def test_given_progressive_when_generate_then_backpack_upgrades_are_locations(self):
world_options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive}
multi_world = setup_solo_multiworld(world_options)
locations = {locations.name for locations in multi_world.get_locations(1)}
assert "Large Pack" in locations
assert "Deluxe Pack" in locations
self.assertIn("Large Pack", locations)
self.assertIn("Deluxe Pack", locations)
def test_given_early_progressive_when_generate_then_progressive_backpack_is_in_early_pool(self):
world_options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive}
world_options = {
options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive}
multi_world = setup_solo_multiworld(world_options)
assert "Progressive Backpack" in multi_world.early_items[1]
self.assertIn("Progressive Backpack", multi_world.early_items[1])
class TestToolProgression(SVTestBase):
@@ -130,25 +149,147 @@ class TestToolProgression(SVTestBase):
items = {item.name for item in multi_world.get_items()}
for tool in TOOLS:
assert tool not in items
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 = {options.ToolProgression.internal_name: options.ToolProgression.option_progressive}
multi_world = setup_solo_multiworld(world_options)
items = [item.name for item in multi_world.get_items()]
for tool in TOOLS:
assert items.count("Progressive " + tool) == 4
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}
multi_world = setup_solo_multiworld(world_options)
locations = {locations.name for locations in multi_world.get_locations(1)}
for material, tool in itertools.product(["Copper", "Iron", "Gold", "Iridium"],
["Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can"]):
assert f"{material} {tool} Upgrade" in locations
assert "Purchase Training Rod" in locations
assert "Bamboo Pole Cutscene" in locations
assert "Purchase Fiberglass Rod" in locations
assert "Purchase Iridium Rod" in locations
for material, tool in itertools.product(ToolMaterial.tiers.values(),
[Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can]):
if material == ToolMaterial.basic:
continue
self.assertIn(f"{material} {tool} Upgrade", locations)
self.assertIn("Purchase Training Rod", locations)
self.assertIn("Bamboo Pole Cutscene", locations)
self.assertIn("Purchase Fiberglass Rod", locations)
self.assertIn("Purchase Iridium Rod", locations)
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:
continue
for value in option.special_range_names:
with self.subTest(f"{option.internal_name}: {value}"):
multiworld = setup_solo_multiworld(
{options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
option.internal_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:
continue
for value in option.options:
with self.subTest(f"{option.internal_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:
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
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],
island_option.internal_name: island_option.options[value]})
self.assertEqual(multiworld.worlds[self.player].options[island_option.internal_name], 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})
multi_world = setup_solo_multiworld(world_options)
trap_items = [item_data.name for item_data in items_by_group[Group.TRAP]]
multiworld_items = [item.name for item in multi_world.get_items()]
for item in trap_items:
with self.subTest(f"{item}"):
self.assertNotIn(item, multiworld_items)
def test_given_traps_when_generate_then_all_traps_in_pool(self):
trap_option = options.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]})
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()]
for item in trap_items:
with self.subTest(f"Option: {value}, Item: {item}"):
self.assertIn(item, multiworld_items)
class TestSpecialOrders(SVTestBase):
def test_given_disabled_then_no_order_in_pool(self):
world_options = {options.SpecialOrderLocations.internal_name: options.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}
for location_name in locations_in_pool:
location = location_table[location_name]
self.assertNotIn(LocationTags.SPECIAL_ORDER_BOARD, location.tags)
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}
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}
for location_name in locations_in_pool:
location = location_table[location_name]
self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags)
for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]:
if board_location.mod_name:
continue
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}
multi_world = setup_solo_multiworld(world_options)
locations_in_pool = {location.name for location in multi_world.get_locations()}
for qi_location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI]:
if qi_location.mod_name:
continue
self.assertIn(qi_location.name, locations_in_pool)
for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]:
if board_location.mod_name:
continue
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}
multi_world = setup_solo_multiworld(world_options)
locations_in_pool = {location.name for location in multi_world.get_locations()}
self.assertNotIn(SpecialOrder.lets_play_a_game, locations_in_pool)