From 24471ceff1454df347c95f28fde0c1bb73adcb6c Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 2 Sep 2022 16:37:23 -0500 Subject: [PATCH 01/88] map option objects to a `World.options` dict --- BaseClasses.py | 3 ++- worlds/AutoWorld.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index cea1d48e6f07..7609875e4b34 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -204,10 +204,11 @@ def set_options(self, args): for player in self.player_ids: self.custom_data[player] = {} world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] + self.worlds[player] = world_type(self, player) for option_key in world_type.option_definitions: setattr(self, option_key, getattr(args, option_key, {})) + self.worlds[player].options[option_key] = getattr(args, option_key)[player] - self.worlds[player] = world_type(self, player) def set_item_links(self): item_links = {} diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 8d9a1b08299b..24d08c70932e 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -121,6 +121,7 @@ class World(metaclass=AutoWorldRegister): A Game should have its own subclass of World in which it defines the required data structures.""" option_definitions: Dict[str, Option[Any]] = {} # link your Options mapping + options: Dict[str, Option[Any]] = {} # option names to resulting option object game: str # name the game topology_present: bool = False # indicate if world type has any meaningful layout/pathing From 220571f8c045e48374de7f3f49f1d670562c314b Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 2 Sep 2022 16:37:37 -0500 Subject: [PATCH 02/88] convert RoR2 to options dict system for testing --- worlds/ror2/__init__.py | 81 ++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index af65a15ea4c5..bcc9f174dc0e 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -42,18 +42,17 @@ class RiskOfRainWorld(World): def generate_early(self) -> None: # figure out how many revivals should exist in the pool - self.total_revivals = int(self.world.total_revivals[self.player].value / 100 * - self.world.total_locations[self.player].value) + self.total_revivals = int(self.options["total_revivals"].value // 100 * self.options["total_locations"].value) def generate_basic(self) -> None: # shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend - if self.world.start_with_revive[self.player].value: + if self.options["start_with_revive"].value: self.world.push_precollected(self.world.create_item("Dio's Best Friend", self.player)) # if presets are enabled generate junk_pool from the selected preset - pool_option = self.world.item_weights[self.player].value + pool_option = self.options["item_weights"].value junk_pool: Dict[str, int] = {} - if self.world.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(): @@ -62,20 +61,20 @@ def generate_basic(self) -> None: junk_pool = item_pool_weights[pool_option].copy() else: # generate junk pool from user created presets junk_pool = { - "Item Scrap, Green": self.world.green_scrap[self.player].value, - "Item Scrap, Red": self.world.red_scrap[self.player].value, - "Item Scrap, Yellow": self.world.yellow_scrap[self.player].value, - "Item Scrap, White": self.world.white_scrap[self.player].value, - "Common Item": self.world.common_item[self.player].value, - "Uncommon Item": self.world.uncommon_item[self.player].value, - "Legendary Item": self.world.legendary_item[self.player].value, - "Boss Item": self.world.boss_item[self.player].value, - "Lunar Item": self.world.lunar_item[self.player].value, - "Equipment": self.world.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, + "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.world.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") # Generate item pool @@ -85,7 +84,7 @@ def generate_basic(self) -> None: # Fill remaining items with randomly generated junk itempool += self.world.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()), - k=self.world.total_locations[self.player].value - self.total_revivals) + k=self.options["total_locations"].value - self.total_revivals) # Convert itempool into real items itempool = list(map(lambda name: self.create_item(name), itempool)) @@ -98,7 +97,7 @@ def set_rules(self) -> None: def create_regions(self) -> None: menu = create_region(self.world, self.player, "Menu") petrichor = create_region(self.world, self.player, "Petrichor V", - [f"ItemPickup{i + 1}" for i in range(self.world.total_locations[self.player].value)]) + [f"ItemPickup{i + 1}" for i in range(self.options["total_locations"].value)]) connection = Entrance(self.player, "Lobby", menu) menu.exits.append(connection) @@ -106,16 +105,16 @@ def create_regions(self) -> None: self.world.regions += [menu, petrichor] - create_events(self.world, self.player) + self.create_events() def fill_slot_data(self): return { - "itemPickupStep": self.world.item_pickup_step[self.player].value, + "itemPickupStep": self.options["item_pickup_step"].value, "seed": "".join(self.world.slot_seeds[self.player].choice(string.digits) for _ in range(16)), - "totalLocations": self.world.total_locations[self.player].value, - "totalRevivals": self.world.total_revivals[self.player].value, - "startWithDio": self.world.start_with_revive[self.player].value, - "FinalStageDeath": self.world.final_stage_death[self.player].value + "totalLocations": self.options["total_locations"].value, + "totalRevivals": self.options["total_revivals"].value, + "startWithDio": self.options["start_with_revive"].value, + "FinalStageDeath": self.options["final_stage_death"].value } def create_item(self, name: str) -> Item: @@ -129,23 +128,23 @@ def create_item(self, name: str) -> Item: item = RiskOfRainItem(name, classification, item_id, self.player) return item - -def create_events(world: MultiWorld, player: int) -> None: - total_locations = world.total_locations[player].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) - - for i in range(num_of_events): - event_loc = RiskOfRainLocation(player, f"Pickup{(i + 1) * 25}", None, world_region) - event_loc.place_locked_item(RiskOfRainItem(f"Pickup{(i + 1) * 25}", ItemClassification.progression, None, player)) - event_loc.access_rule(lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", player)) - world_region.locations.append(event_loc) - - victory_event = RiskOfRainLocation(player, "Victory", None, world_region) - victory_event.place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, None, player)) - world_region.locations.append(victory_event) + def create_events(self) -> None: + total_locations = self.options["total_locations"].value + num_of_events = total_locations // 25 + if total_locations / 25 == num_of_events: + num_of_events -= 1 + world_region = self.world.get_region("Petrichor V", self.player) + + for i in range(num_of_events): + event_loc = RiskOfRainLocation(self.player, f"Pickup{(i + 1) * 25}", None, world_region) + event_loc.place_locked_item(RiskOfRainItem(f"Pickup{(i + 1) * 25}", + ItemClassification.progression, None, self.player)) + event_loc.access_rule(lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", self.player)) + world_region.locations.append(event_loc) + + victory_event = RiskOfRainLocation(self.player, "Victory", None, world_region) + victory_event.place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, None, self.player)) + world_region.locations.append(victory_event) def create_region(world: MultiWorld, player: int, name: str, locations: List[str] = None) -> Region: From 643205cfe172624323658d402b8a1da99999906c Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 2 Sep 2022 17:42:16 -0500 Subject: [PATCH 03/88] add temp behavior for lttp with notes --- BaseClasses.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 7609875e4b34..4be26d671b33 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -196,18 +196,27 @@ def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optio range(1, self.players + 1)} def set_options(self, args): - 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]] self.worlds[player] = world_type(self, player) + for option_key in Options.common_options: + option_values = getattr(args, option_key, {}) + setattr(self, option_key, option_values) + if player in option_values: + self.worlds[player].options[option_key] = option_values[player] + + for option_key in Options.per_game_common_options: + option_values = getattr(args, option_key, {}) + setattr(self, option_key, option_values) + if player in option_values: + self.worlds[player].options[option_key] = option_values[player] + for option_key in world_type.option_definitions: - setattr(self, option_key, getattr(args, option_key, {})) - self.worlds[player].options[option_key] = getattr(args, option_key)[player] + option_values = getattr(args, option_key, {}) # TODO remove {} after old lttp options + setattr(self, option_key, option_values) # TODO rip out around 0.4.0 + if player in option_values: # TODO more lttp jank + self.worlds[player].options[option_key] = option_values[player] def set_item_links(self): From 695c94cb8797c645795a4947a18f9f1b59991463 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 24 Sep 2022 19:13:30 -0500 Subject: [PATCH 04/88] copy/paste bad --- BaseClasses.py | 17 +++-------------- worlds/AutoWorld.py | 3 ++- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 4be26d671b33..747ee9bd1999 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,6 +1,7 @@ from __future__ import annotations import copy +import itertools from enum import unique, IntEnum, IntFlag import logging import json @@ -200,25 +201,13 @@ def set_options(self, args): self.custom_data[player] = {} world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] self.worlds[player] = world_type(self, player) - for option_key in Options.common_options: + for option_key in itertools.chain(Options.common_options, Options.per_game_common_options, + world_type.option_definitions): option_values = getattr(args, option_key, {}) setattr(self, option_key, option_values) if player in option_values: self.worlds[player].options[option_key] = option_values[player] - for option_key in Options.per_game_common_options: - option_values = getattr(args, option_key, {}) - setattr(self, option_key, option_values) - if player in option_values: - self.worlds[player].options[option_key] = option_values[player] - - for option_key in world_type.option_definitions: - option_values = getattr(args, option_key, {}) # TODO remove {} after old lttp options - setattr(self, option_key, option_values) # TODO rip out around 0.4.0 - if player in option_values: # TODO more lttp jank - self.worlds[player].options[option_key] = option_values[player] - - def set_item_links(self): item_links = {} diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 24d08c70932e..446253296b0a 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -121,7 +121,7 @@ class World(metaclass=AutoWorldRegister): A Game should have its own subclass of World in which it defines the required data structures.""" option_definitions: Dict[str, Option[Any]] = {} # link your Options mapping - options: Dict[str, Option[Any]] = {} # option names to resulting option object + options: Dict[str, Option[Any]] # option names to resulting option object game: str # name the game topology_present: bool = False # indicate if world type has any meaningful layout/pathing @@ -189,6 +189,7 @@ class World(metaclass=AutoWorldRegister): def __init__(self, world: "MultiWorld", player: int): self.world = world self.player = player + self.options = {} # overridable methods that get called by Main.py, sorted by execution order # can also be implemented as a classmethod and called "stage_", From d481eed2587248e066fef77ccdfce4fcf85fcac7 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 29 Sep 2022 11:17:14 -0500 Subject: [PATCH 05/88] convert `set_default_common_options` to a namespace property --- BaseClasses.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 1d29b228d64a..2ce2ad289298 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2,6 +2,7 @@ import copy import itertools +from argparse import Namespace from enum import unique, IntEnum, IntFlag import logging import json @@ -261,12 +262,13 @@ def set_item_links(self): group["non_local_items"] = item_link["non_local_items"] # 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}) + @property + def default_common_options(self) -> Namespace: self.state = CollectionState(self) + args = Namespace() + for option_key, option in itertools.chain(Options.common_options.items(), Options.per_game_common_options.items()): + setattr(args, option_key, {player_id: option.from_any(option.default) for player_id in self.player_ids}) + return args def secure(self): self.random = secrets.SystemRandom() From f9a8bb2bb7bbb36519ce312cf24b1156eb2c68ef Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 29 Sep 2022 11:17:50 -0500 Subject: [PATCH 06/88] reorganize test call order --- test/dungeons/TestDungeon.py | 3 +-- test/general/__init__.py | 3 +-- test/inverted/TestInverted.py | 3 +-- test/inverted/TestInvertedBombRules.py | 3 +-- test/inverted_minor_glitches/TestInvertedMinor.py | 3 +-- test/inverted_owg/TestInvertedOWG.py | 3 +-- test/minor_glitches/TestMinor.py | 3 +-- test/owg/TestVanillaOWG.py | 3 +-- test/vanilla/TestVanilla.py | 3 +-- 9 files changed, 9 insertions(+), 18 deletions(-) diff --git a/test/dungeons/TestDungeon.py b/test/dungeons/TestDungeon.py index 0568e799f259..04cfbec27127 100644 --- a/test/dungeons/TestDungeon.py +++ b/test/dungeons/TestDungeon.py @@ -15,11 +15,10 @@ class TestDungeon(unittest.TestCase): def setUp(self): self.world = MultiWorld(1) - args = Namespace() + args = self.world.default_common_options 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.world.set_options(args) - self.world.set_default_common_options() self.starting_regions = [] # Where to start exploring self.remove_exits = [] # Block dungeon exits self.world.difficulty_requirements[1] = difficulties['normal'] diff --git a/test/general/__init__.py b/test/general/__init__.py index 479f4af520f0..6691b9421f84 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -11,11 +11,10 @@ def setup_default_world(world_type) -> MultiWorld: world.game[1] = world_type.game world.player_name = {1: "Tester"} world.set_seed() - args = Namespace() + args = world.default_common_options for name, option in world_type.option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) world.set_options(args) - world.set_default_common_options() for step in gen_steps: call_all(world, step) return world diff --git a/test/inverted/TestInverted.py b/test/inverted/TestInverted.py index 0c96f0b26dbd..856b97f5905e 100644 --- a/test/inverted/TestInverted.py +++ b/test/inverted/TestInverted.py @@ -15,11 +15,10 @@ class TestInverted(TestBase): def setUp(self): self.world = MultiWorld(1) - args = Namespace() + args = self.world.default_common_options 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.world.set_options(args) - self.world.set_default_common_options() self.world.difficulty_requirements[1] = difficulties['normal'] self.world.mode[1] = "inverted" create_inverted_regions(self.world, 1) diff --git a/test/inverted/TestInvertedBombRules.py b/test/inverted/TestInvertedBombRules.py index f6afa9d0dc16..9cccdc93ffcf 100644 --- a/test/inverted/TestInvertedBombRules.py +++ b/test/inverted/TestInvertedBombRules.py @@ -16,11 +16,10 @@ class TestInvertedBombRules(unittest.TestCase): def setUp(self): self.world = MultiWorld(1) self.world.mode[1] = "inverted" - args = Namespace + args = self.world.default_common_options 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.world.set_options(args) - self.world.set_default_common_options() self.world.difficulty_requirements[1] = difficulties['normal'] create_inverted_regions(self.world, 1) create_dungeons(self.world, 1) diff --git a/test/inverted_minor_glitches/TestInvertedMinor.py b/test/inverted_minor_glitches/TestInvertedMinor.py index 42e7c942d682..a51ba143cdee 100644 --- a/test/inverted_minor_glitches/TestInvertedMinor.py +++ b/test/inverted_minor_glitches/TestInvertedMinor.py @@ -16,11 +16,10 @@ class TestInvertedMinor(TestBase): def setUp(self): self.world = MultiWorld(1) - args = Namespace() + args = self.world.default_common_options 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.world.set_options(args) - self.world.set_default_common_options() self.world.mode[1] = "inverted" self.world.logic[1] = "minorglitches" self.world.difficulty_requirements[1] = difficulties['normal'] diff --git a/test/inverted_owg/TestInvertedOWG.py b/test/inverted_owg/TestInvertedOWG.py index 064dd9e08395..f7ca5f3bdcb5 100644 --- a/test/inverted_owg/TestInvertedOWG.py +++ b/test/inverted_owg/TestInvertedOWG.py @@ -17,11 +17,10 @@ class TestInvertedOWG(TestBase): def setUp(self): self.world = MultiWorld(1) - args = Namespace() + args = self.world.default_common_options 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.world.set_options(args) - self.world.set_default_common_options() self.world.logic[1] = "owglitches" self.world.mode[1] = "inverted" self.world.difficulty_requirements[1] = difficulties['normal'] diff --git a/test/minor_glitches/TestMinor.py b/test/minor_glitches/TestMinor.py index 81c09cfb2789..b16c4ce353e0 100644 --- a/test/minor_glitches/TestMinor.py +++ b/test/minor_glitches/TestMinor.py @@ -16,11 +16,10 @@ class TestMinor(TestBase): def setUp(self): self.world = MultiWorld(1) - args = Namespace() + args = self.world.default_common_options 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.world.set_options(args) - self.world.set_default_common_options() self.world.logic[1] = "minorglitches" self.world.difficulty_requirements[1] = difficulties['normal'] create_regions(self.world, 1) diff --git a/test/owg/TestVanillaOWG.py b/test/owg/TestVanillaOWG.py index e5489117a71f..1d11a63b2934 100644 --- a/test/owg/TestVanillaOWG.py +++ b/test/owg/TestVanillaOWG.py @@ -17,11 +17,10 @@ class TestVanillaOWG(TestBase): def setUp(self): self.world = MultiWorld(1) - args = Namespace() + args = self.world.default_common_options 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.world.set_options(args) - self.world.set_default_common_options() self.world.difficulty_requirements[1] = difficulties['normal'] self.world.logic[1] = "owglitches" create_regions(self.world, 1) diff --git a/test/vanilla/TestVanilla.py b/test/vanilla/TestVanilla.py index e5ee73406aac..368f0f127658 100644 --- a/test/vanilla/TestVanilla.py +++ b/test/vanilla/TestVanilla.py @@ -15,11 +15,10 @@ class TestVanilla(TestBase): def setUp(self): self.world = MultiWorld(1) - args = Namespace() + args = self.world.default_common_options 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.world.set_options(args) - self.world.set_default_common_options() self.world.logic[1] = "noglitches" self.world.difficulty_requirements[1] = difficulties['normal'] create_regions(self.world, 1) From cded105e6c881db3b76b39279e4fb1a6829fb123 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 29 Sep 2022 11:18:12 -0500 Subject: [PATCH 07/88] have fill_restrictive use the new options system --- Fill.py | 3 ++- test/general/TestFill.py | 13 +++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Fill.py b/Fill.py index c62eaabde8bb..8ec6efaef3b2 100644 --- a/Fill.py +++ b/Fill.py @@ -5,6 +5,7 @@ from collections import Counter, deque from BaseClasses import CollectionState, Location, LocationProgressType, MultiWorld, Item +from Options import Accessibility from worlds.AutoWorld import call_all @@ -52,7 +53,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 diff --git a/test/general/TestFill.py b/test/general/TestFill.py index 8ce5b3b2816f..ead95c1e3860 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -1,5 +1,9 @@ +import itertools 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_items_restrictive from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, RegionType, Item, Location, \ @@ -10,6 +14,7 @@ def generate_multi_world(players: int = 1) -> MultiWorld: multi_world = MultiWorld(players) multi_world.player_name = {} + args = multi_world.default_common_options for i in range(players): player_id = i+1 world = World(multi_world, player_id) @@ -20,8 +25,12 @@ def generate_multi_world(players: int = 1) -> MultiWorld: "Menu Region Hint", player_id, multi_world) multi_world.regions.append(region) + for option_key in itertools.chain(Options.common_options, Options.per_game_common_options): + option_value = getattr(args, option_key, {}) + setattr(multi_world, option_key, option_value) + multi_world.worlds[player_id].options[option_key] = option_value[player_id] + multi_world.set_seed(0) - multi_world.set_default_common_options() return multi_world @@ -187,7 +196,7 @@ def test_minimal_fill(self): 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( From 52290df9449191a718018cd0c063004e2483cbb5 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 18 Oct 2022 10:34:14 -0500 Subject: [PATCH 08/88] update world api --- docs/world api.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/world api.md b/docs/world api.md index cf26cfd967a3..f37b015d38a3 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -85,7 +85,7 @@ inside a World object. ### Player Options Players provide customized settings for their World in the form of yamls. -Those are accessible through `self.world.[self.player]`. A dict +Those are accessible through `self.world.options[""]`. A dict of valid options has to be provided in `self.option_definitions`. Options are automatically added to the `World` object for easy access. @@ -210,7 +210,7 @@ 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 -`world/[world_name]/Options.py` and make its `mygame_options` accesible. +`world/[world_name]/Options.py` and make its `mygame_options` accessible. When imported names pile up it may be easier to use `from . import Options` and access the variable as `Options.mygame_options`. @@ -262,7 +262,8 @@ 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, Option]`, that is -assigned to the world under `self.option_definitions`. +assigned to the world under `self.option_definitions`. By convention, the string +that defines your option should be in `snake_case`. Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`. For more see `Options.py` in AP's base directory. @@ -455,7 +456,7 @@ In addition, the following methods can be implemented and attributes can be set ```python def generate_early(self) -> None: # read player settings to world instance - self.final_boss_hp = self.world.final_boss_hp[self.player].value + self.final_boss_hp = self.world.options["final_boss_hp"].value ``` #### create_item From 5896e6ca92b374c6cf099bbeaf38dd14d4cf856c Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 18 Oct 2022 11:05:52 -0500 Subject: [PATCH 09/88] update soe tests --- test/soe/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/soe/__init__.py b/test/soe/__init__.py index 0161a6c32fbc..6f4d41aff1dc 100644 --- a/test/soe/__init__.py +++ b/test/soe/__init__.py @@ -17,11 +17,10 @@ def setUp(self): self.world.game[1] = self.game self.world.player_name = {1: "Tester"} self.world.set_seed() - args = Namespace() + args = self.world.default_common_options for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].option_definitions.items(): setattr(args, name, {1: option.from_any(self.options.get(name, option.default))}) self.world.set_options(args) - self.world.set_default_common_options() for step in gen_steps: call_all(self.world, step) From 2581d574978f6d20f629203e7afe66e9f6a6fdaa Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 29 Oct 2022 13:24:34 -0500 Subject: [PATCH 10/88] fix world api --- docs/world api.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/world api.md b/docs/world api.md index f37b015d38a3..5cc7f9148195 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -85,7 +85,7 @@ inside a World object. ### Player Options Players provide customized settings for their World in the form of yamls. -Those are accessible through `self.world.options[""]`. A dict +Those are accessible through `self.options[""]`. A dict of valid options has to be provided in `self.option_definitions`. Options are automatically added to the `World` object for easy access. @@ -456,7 +456,7 @@ In addition, the following methods can be implemented and attributes can be set ```python def generate_early(self) -> None: # read player settings to world instance - self.final_boss_hp = self.world.options["final_boss_hp"].value + self.final_boss_hp = self.options["final_boss_hp"].value ``` #### create_item @@ -676,9 +676,9 @@ def generate_output(self, output_directory: str): in self.world.precollected_items[self.player]], "final_boss_hp": self.final_boss_hp, # store option name "easy", "normal" or "hard" for difficuly - "difficulty": self.world.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.world.fix_xyz_glitch[self.player].value + "fix_xyz_glitch": self.options["fix_xyz_glitch"].value } # point to a ROM specified by the installation src = Utils.get_options()["mygame_options"]["rom_file"] From 121152be5bc0f41ad690b575ac11ef49c97e6a6e Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:20:08 +0100 Subject: [PATCH 11/88] core: auto initialize a dataclass on the World class with the option results --- BaseClasses.py | 10 +++++--- Fill.py | 2 +- Options.py | 42 +++++++++++++++++++------------ docs/world api.md | 53 ++++++++++++++++++++------------------- test/general/TestFill.py | 8 +++--- worlds/AutoWorld.py | 9 ++++--- worlds/ror2/Options.py | 54 ++++++++++++++++++++-------------------- worlds/ror2/__init__.py | 54 +++++++++++++++++++++------------------- 8 files changed, 126 insertions(+), 106 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index aed336a24e09..a59e1b415272 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -3,13 +3,13 @@ import copy import itertools -from argparse import Namespace from enum import unique, IntEnum, IntFlag import logging import json import functools from collections import OrderedDict, Counter, deque -from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple +from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple, Type, \ + get_type_hints import typing # this can go away when Python 3.8 support is dropped import secrets import random @@ -214,8 +214,10 @@ def set_options(self, args: Namespace) -> None: world_type.option_definitions): option_values = getattr(args, option_key, {}) setattr(self, option_key, option_values) - if player in option_values: - self.worlds[player].options[option_key] = option_values[player] + # TODO - remove this loop once all worlds use options dataclasses + options_dataclass: Type[Options.PerGameCommonOptions] = self.worlds[player].options_dataclass + self.worlds[player].o = options_dataclass(**{option_key: getattr(args, option_key)[player] + for option_key in get_type_hints(options_dataclass)}) def set_item_links(self): item_links = {} diff --git a/Fill.py b/Fill.py index cb8d2a878741..2d6647dea603 100644 --- a/Fill.py +++ b/Fill.py @@ -55,7 +55,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.worlds[item_to_place.player].options["accessibility"] == Accessibility.option_minimal: + if world.worlds[item_to_place.player].o.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 diff --git a/Options.py b/Options.py index ad87f5ebf8d9..3876e59c324c 100644 --- a/Options.py +++ b/Options.py @@ -1,6 +1,7 @@ from __future__ import annotations import abc from copy import deepcopy +from dataclasses import dataclass import math import numbers import typing @@ -862,10 +863,13 @@ class ProgressionBalancing(SpecialRange): } -common_options = { - "progression_balancing": ProgressionBalancing, - "accessibility": Accessibility -} +@dataclass +class CommonOptions: + progression_balancing: ProgressionBalancing + accessibility: Accessibility + +common_options = typing.get_type_hints(CommonOptions) +# TODO - remove this dict once all worlds use options dataclasses class ItemSet(OptionSet): @@ -982,18 +986,24 @@ def verify(self, world, player_name: str, plando_options) -> None: raise Exception(f"item_link {link['name']} has {intersection} items in both its local_items and non_local_items pool.") -per_game_common_options = { - **common_options, # can be overwritten per-game - "local_items": LocalItems, - "non_local_items": NonLocalItems, - "early_items": EarlyItems, - "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 + early_items: EarlyItems + start_inventory: StartInventory + start_hints: StartHints + start_location_hints: StartLocationHints + exclude_locations: ExcludeLocations + priority_locations: PriorityLocations + item_links: ItemLinks + +per_game_common_options = typing.get_type_hints(PerGameCommonOptions) +# TODO - remove this dict once all worlds use options dataclasses + + +GameOptions = typing.TypeVar("GameOptions", bound=PerGameCommonOptions) + if __name__ == "__main__": diff --git a/docs/world api.md b/docs/world api.md index 5cc7f9148195..6733ca093e0d 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -85,9 +85,10 @@ inside a World object. ### Player Options Players provide customized settings for their World in the form of yamls. -Those are accessible through `self.options[""]`. 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.o.`. ### World Options @@ -209,11 +210,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 -`world/[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 @@ -261,9 +262,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, Option]`, that is -assigned to the world under `self.option_definitions`. By convention, the string -that defines your option should be in `snake_case`. +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. @@ -298,8 +299,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.""" @@ -322,25 +323,26 @@ class FixXYZGlitch(Toggle): """Fixes ABC when you do XYZ""" display_name = "Fix XYZ Glitch" -# By convention we call the options dict variable `_options`. -mygame_options: typing.Dict[str, type(Option)] = { - "difficulty": Difficulty, - "final_boss_hp": FinalBossHP, - "fix_xyz_glitch": FixXYZGlitch -} +# By convention, we call the options dataclass `Options`. +@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 + o: MyGameOptions # typing for option results #... ``` - + ### Local or Remote A world with `remote_items` set to `True` gets all items items from the server @@ -358,7 +360,7 @@ more natural. These games typically have been edited to 'bake in' the items. ```python # world/mygame/__init__.py -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 @@ -374,7 +376,8 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation class MyGameWorld(World): """Insert description of the world/game here.""" game: str = "My Game" # name of the game/world - option_definitions = mygame_options # options the player can set + options_dataclass = MyGameOptions # options the player can set + o: MyGameOptions # typing for option results topology_present: bool = True # show path to required location checks in spoiler remote_items: bool = False # True if all items come from the server remote_start_inventory: bool = False # True if start inventory comes from the server @@ -456,7 +459,7 @@ In addition, the following methods can be implemented and attributes can be set ```python def generate_early(self) -> None: # read player settings to world instance - self.final_boss_hp = self.options["final_boss_hp"].value + self.final_boss_hp = self.o.final_boss_hp.value ``` #### create_item @@ -676,9 +679,9 @@ def generate_output(self, output_directory: str): in self.world.precollected_items[self.player]], "final_boss_hp": self.final_boss_hp, # store option name "easy", "normal" or "hard" for difficuly - "difficulty": self.options["difficulty"].current_key, + "difficulty": self.o.difficulty.current_key, # store option value True or False for fixing a glitch - "fix_xyz_glitch": self.options["fix_xyz_glitch"].value + "fix_xyz_glitch": self.o.fix_xyz_glitch.value } # point to a ROM specified by the installation src = Utils.get_options()["mygame_options"]["rom_file"] diff --git a/test/general/TestFill.py b/test/general/TestFill.py index 2ccc5430df72..ad8261ee8f2c 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -1,5 +1,5 @@ import itertools -from typing import List, Iterable +from typing import get_type_hints, List, Iterable import unittest import Options @@ -28,7 +28,9 @@ def generate_multi_world(players: int = 1) -> MultiWorld: for option_key in itertools.chain(Options.common_options, Options.per_game_common_options): option_value = getattr(args, option_key, {}) setattr(multi_world, option_key, option_value) - multi_world.worlds[player_id].options[option_key] = option_value[player_id] + # TODO - remove this loop once all worlds use options dataclasses + world.o = world.options_dataclass(**{option_key: getattr(args, option_key)[player_id] + for option_key in get_type_hints(world.options_dataclass)}) multi_world.set_seed(0) @@ -196,7 +198,7 @@ def test_minimal_fill(self): items = player1.prog_items locations = player1.locations - multi_world.worlds[player1.id].options["accessibility"] = Accessibility.from_any(Accessibility.option_minimal) + multi_world.worlds[player1.id].o.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( diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 943c25b59f3f..6f0c2029abd7 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -5,7 +5,7 @@ import pathlib from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Type, Union, TYPE_CHECKING -from Options import AssembleOptions, Option +from Options import AssembleOptions, GameOptions, PerGameCommonOptions from BaseClasses import CollectionState if TYPE_CHECKING: @@ -130,8 +130,10 @@ 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: Dict[str, AssembleOptions] = {} # link your Options mapping - options: Dict[str, Option[Any]] # automatically populated option names to resulting option object + option_definitions: Dict[str, AssembleOptions] = {} # TODO - remove this once all worlds use options dataclasses + options_dataclass: Type[GameOptions] = PerGameCommonOptions # link your Options mapping + o: PerGameCommonOptions + game: str # name the game topology_present: bool = False # indicate if world type has any meaningful layout/pathing @@ -199,7 +201,6 @@ class World(metaclass=AutoWorldRegister): def __init__(self, world: "MultiWorld", player: int): self.world = world self.player = player - self.options = {} # overridable methods that get called by Main.py, sorted by execution order # can also be implemented as a classmethod and called "stage_", diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py index a95cbf597ae4..fd61b951d3d7 100644 --- a/worlds/ror2/Options.py +++ b/worlds/ror2/Options.py @@ -1,5 +1,5 @@ -from typing import Dict -from Options import Option, DefaultOnToggle, Range, Choice +from dataclasses import dataclass +from Options import DefaultOnToggle, Range, Choice, PerGameCommonOptions class TotalLocations(Range): @@ -150,28 +150,28 @@ class ItemWeights(Choice): option_scraps_only = 8 -# 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, - "equipment": Equipment -} - -ror2_options: Dict[str, type(Option)] = { - "total_locations": TotalLocations, - "total_revivals": TotalRevivals, - "start_with_revive": StartWithRevive, - "final_stage_death": FinalStageDeath, - "item_pickup_step": ItemPickupStep, - "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 + equipment: Equipment + + +@dataclass +class ROR2Options(PerGameCommonOptions, ROR2Weights): + total_locations: TotalLocations + total_revivals: TotalRevivals + start_with_revive: StartWithRevive + final_stage_death: FinalStageDeath + item_pickup_step: ItemPickupStep + enable_lunar: AllowLunarItems + item_weights: ItemWeights + item_pool_presets: ItemPoolPresetToggle diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index bcc9f174dc0e..c420f3e7bd26 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -1,11 +1,11 @@ import string -from typing import Dict, List +from typing import Dict, get_type_hints, List from .Items import RiskOfRainItem, item_table, item_pool_weights from .Locations import RiskOfRainLocation, item_pickups from .Rules import set_rules from BaseClasses import Region, RegionType, Entrance, Item, ItemClassification, MultiWorld, Tutorial -from .Options import ror2_options, ItemWeights +from .Options import ItemWeights, ROR2Options from worlds.AutoWorld import World, WebWorld client_version = 1 @@ -29,7 +29,9 @@ class RiskOfRainWorld(World): first crash landing. """ game: str = "Risk of Rain 2" - option_definitions = ror2_options + option_definitions = get_type_hints(ROR2Options) + options_dataclass = ROR2Options + o: ROR2Options topology_present = False item_name_to_id = item_table @@ -42,17 +44,17 @@ class RiskOfRainWorld(World): def generate_early(self) -> None: # figure out how many revivals should exist in the pool - self.total_revivals = int(self.options["total_revivals"].value // 100 * self.options["total_locations"].value) + self.total_revivals = int(self.o.total_revivals.value // 100 * self.o.total_locations.value) def generate_basic(self) -> None: # shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend - if self.options["start_with_revive"].value: + if self.o.start_with_revive.value: self.world.push_precollected(self.world.create_item("Dio's Best Friend", self.player)) # if presets are enabled generate junk_pool from the selected preset - pool_option = self.options["item_weights"].value + pool_option = self.o.item_weights.value junk_pool: Dict[str, int] = {} - if self.options["item_pool_presets"]: + if self.o.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(): @@ -61,20 +63,20 @@ def generate_basic(self) -> None: junk_pool = item_pool_weights[pool_option].copy() else: # generate junk pool from user created presets junk_pool = { - "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, - "Equipment": self.options["equipment"].value + "Item Scrap, Green": self.o.green_scrap.value, + "Item Scrap, Red": self.o.red_scrap.value, + "Item Scrap, Yellow": self.o.yellow_scrap.value, + "Item Scrap, White": self.o.white_scrap.value, + "Common Item": self.o.common_item.value, + "Uncommon Item": self.o.uncommon_item.value, + "Legendary Item": self.o.legendary_item.value, + "Boss Item": self.o.boss_item.value, + "Lunar Item": self.o.lunar_item.value, + "Equipment": self.o.equipment.value } # remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled - if not (self.options["enable_lunar"] or pool_option == ItemWeights.option_lunartic): + if not (self.o.enable_lunar or pool_option == ItemWeights.option_lunartic): junk_pool.pop("Lunar Item") # Generate item pool @@ -84,7 +86,7 @@ def generate_basic(self) -> None: # Fill remaining items with randomly generated junk itempool += self.world.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()), - k=self.options["total_locations"].value - self.total_revivals) + k=self.o.total_locations.value - self.total_revivals) # Convert itempool into real items itempool = list(map(lambda name: self.create_item(name), itempool)) @@ -97,7 +99,7 @@ def set_rules(self) -> None: def create_regions(self) -> None: menu = create_region(self.world, self.player, "Menu") petrichor = create_region(self.world, self.player, "Petrichor V", - [f"ItemPickup{i + 1}" for i in range(self.options["total_locations"].value)]) + [f"ItemPickup{i + 1}" for i in range(self.o.total_locations.value)]) connection = Entrance(self.player, "Lobby", menu) menu.exits.append(connection) @@ -109,12 +111,12 @@ def create_regions(self) -> None: def fill_slot_data(self): return { - "itemPickupStep": self.options["item_pickup_step"].value, + "itemPickupStep": self.o.item_pickup_step.value, "seed": "".join(self.world.slot_seeds[self.player].choice(string.digits) for _ in range(16)), - "totalLocations": self.options["total_locations"].value, - "totalRevivals": self.options["total_revivals"].value, - "startWithDio": self.options["start_with_revive"].value, - "FinalStageDeath": self.options["final_stage_death"].value + "totalLocations": self.o.total_locations.value, + "totalRevivals": self.o.total_revivals.value, + "startWithDio": self.o.start_with_revive.value, + "FinalStageDeath": self.o.final_stage_death.value } def create_item(self, name: str) -> Item: @@ -129,7 +131,7 @@ def create_item(self, name: str) -> Item: return item def create_events(self) -> None: - total_locations = self.options["total_locations"].value + total_locations = self.o.total_locations.value num_of_events = total_locations // 25 if total_locations / 25 == num_of_events: num_of_events -= 1 From 82ff1259e21f74e2e8b30a9c24f6cf0edf9c7890 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:55:02 +0100 Subject: [PATCH 12/88] core: auto initialize a dataclass on the World class with the option results: small tying improvement --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index a59e1b415272..33dbe8d1a36b 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -215,7 +215,7 @@ def set_options(self, args: Namespace) -> None: option_values = getattr(args, option_key, {}) setattr(self, option_key, option_values) # TODO - remove this loop once all worlds use options dataclasses - options_dataclass: Type[Options.PerGameCommonOptions] = self.worlds[player].options_dataclass + options_dataclass: Type[Options.GameOptions] = self.worlds[player].options_dataclass self.worlds[player].o = options_dataclass(**{option_key: getattr(args, option_key)[player] for option_key in get_type_hints(options_dataclass)}) From 43353de5c3777f23aaf94d8754e35b832c6d810c Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 9 Jan 2023 13:10:32 -0600 Subject: [PATCH 13/88] add `as_dict` method to the options dataclass --- Options.py | 10 ++++++++++ docs/world api.md | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Options.py b/Options.py index 3876e59c324c..4d8c2c53b1ed 100644 --- a/Options.py +++ b/Options.py @@ -868,6 +868,16 @@ class CommonOptions: progression_balancing: ProgressionBalancing accessibility: Accessibility + def as_dict(self, *args: str) -> typing.Dict: + option_results = {} + for option_name in args: + if option_name in self.__annotations__: + option_results[option_name] = getattr(self, option_name).value + else: + raise ValueError(f"{option_name} not found in {self.__annotations__}") + return option_results + + common_options = typing.get_type_hints(CommonOptions) # TODO - remove this dict once all worlds use options dataclasses diff --git a/docs/world api.md b/docs/world api.md index 6733ca093e0d..eba22ed0be18 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -88,7 +88,8 @@ Players provide customized settings for their World in the form of yamls. 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.o.`. +Those are accessible through `self.o.`, and you can get a dictionary of the option values via +`self.o.as_dict()`, passing the desired options as strings. ### World Options From 76833d5809a3eb43193e692636dac7119d2a2fe3 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 9 Jan 2023 13:54:18 -0600 Subject: [PATCH 14/88] fix namespace issues with tests --- test/TestBase.py | 3 +-- worlds/alttp/test/dungeons/TestDungeon.py | 2 +- worlds/alttp/test/inverted/TestInverted.py | 3 +-- worlds/alttp/test/inverted/TestInvertedBombRules.py | 2 +- .../alttp/test/inverted_minor_glitches/TestInvertedMinor.py | 3 +-- worlds/alttp/test/inverted_owg/TestInvertedOWG.py | 3 +-- worlds/alttp/test/minor_glitches/TestMinor.py | 3 +-- worlds/alttp/test/owg/TestVanillaOWG.py | 3 +-- worlds/alttp/test/vanilla/TestVanilla.py | 3 +-- worlds/ror2/__init__.py | 6 +++--- 10 files changed, 12 insertions(+), 19 deletions(-) diff --git a/test/TestBase.py b/test/TestBase.py index 8a17232bd1fe..6d1a85b200f4 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -118,13 +118,12 @@ def world_setup(self, seed: typing.Optional[int] = None) -> None: self.multiworld.game[1] = self.game self.multiworld.player_name = {1: "Tester"} self.multiworld.set_seed(seed) - args = Namespace() + args = self.multiworld.default_common_options for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].option_definitions.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) diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py index 4018d0fc0a4e..9641f2ce8ec4 100644 --- a/worlds/alttp/test/dungeons/TestDungeon.py +++ b/worlds/alttp/test/dungeons/TestDungeon.py @@ -15,7 +15,7 @@ class TestDungeon(unittest.TestCase): def setUp(self): self.multiworld = MultiWorld(1) - args = self.world.default_common_options + args = self.multiworld.default_common_options 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) diff --git a/worlds/alttp/test/inverted/TestInverted.py b/worlds/alttp/test/inverted/TestInverted.py index 309a34d54b95..95de61da648e 100644 --- a/worlds/alttp/test/inverted/TestInverted.py +++ b/worlds/alttp/test/inverted/TestInverted.py @@ -15,11 +15,10 @@ class TestInverted(TestBase): def setUp(self): self.multiworld = MultiWorld(1) - args = Namespace() + args = self.multiworld.default_common_options 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'] self.multiworld.mode[1] = "inverted" create_inverted_regions(self.multiworld, 1) diff --git a/worlds/alttp/test/inverted/TestInvertedBombRules.py b/worlds/alttp/test/inverted/TestInvertedBombRules.py index 9d386b33c39a..1bb8a79be767 100644 --- a/worlds/alttp/test/inverted/TestInvertedBombRules.py +++ b/worlds/alttp/test/inverted/TestInvertedBombRules.py @@ -16,7 +16,7 @@ class TestInvertedBombRules(unittest.TestCase): def setUp(self): self.multiworld = MultiWorld(1) self.multiworld.mode[1] = "inverted" - args = self.world.default_common_options + args = self.multiworld.default_common_options 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) diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py index 7ea7980bbe60..f9068da60a8e 100644 --- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py +++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py @@ -16,11 +16,10 @@ class TestInvertedMinor(TestBase): def setUp(self): self.multiworld = MultiWorld(1) - args = Namespace() + args = self.multiworld.default_common_options 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.mode[1] = "inverted" self.multiworld.logic[1] = "minorglitches" self.multiworld.difficulty_requirements[1] = difficulties['normal'] diff --git a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py index 7dae3589296e..78f008680a55 100644 --- a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py +++ b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py @@ -17,11 +17,10 @@ class TestInvertedOWG(TestBase): def setUp(self): self.multiworld = MultiWorld(1) - args = Namespace() + args = self.multiworld.default_common_options 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.logic[1] = "owglitches" self.multiworld.mode[1] = "inverted" self.multiworld.difficulty_requirements[1] = difficulties['normal'] diff --git a/worlds/alttp/test/minor_glitches/TestMinor.py b/worlds/alttp/test/minor_glitches/TestMinor.py index ec92b5563ef1..a0a87e913eeb 100644 --- a/worlds/alttp/test/minor_glitches/TestMinor.py +++ b/worlds/alttp/test/minor_glitches/TestMinor.py @@ -16,11 +16,10 @@ class TestMinor(TestBase): def setUp(self): self.multiworld = MultiWorld(1) - args = self.world.default_common_options + args = self.multiworld.default_common_options 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.logic[1] = "minorglitches" self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.worlds[1].er_seed = 0 diff --git a/worlds/alttp/test/owg/TestVanillaOWG.py b/worlds/alttp/test/owg/TestVanillaOWG.py index a12679b8cfd1..fa288ea5b44e 100644 --- a/worlds/alttp/test/owg/TestVanillaOWG.py +++ b/worlds/alttp/test/owg/TestVanillaOWG.py @@ -17,11 +17,10 @@ class TestVanillaOWG(TestBase): def setUp(self): self.multiworld = MultiWorld(1) - args = Namespace() + args = self.multiworld.default_common_options 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'] self.multiworld.logic[1] = "owglitches" self.multiworld.worlds[1].er_seed = 0 diff --git a/worlds/alttp/test/vanilla/TestVanilla.py b/worlds/alttp/test/vanilla/TestVanilla.py index 32c9c6180049..c2352ebb20e5 100644 --- a/worlds/alttp/test/vanilla/TestVanilla.py +++ b/worlds/alttp/test/vanilla/TestVanilla.py @@ -15,11 +15,10 @@ class TestVanilla(TestBase): def setUp(self): self.multiworld = MultiWorld(1) - args = Namespace() + args = self.multiworld.default_common_options 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.logic[1] = "noglitches" self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.worlds[1].er_seed = 0 diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 12afd79e5327..b50b4e675ddf 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -2,7 +2,7 @@ from typing import Dict, get_type_hints, List from .Items import RiskOfRainItem, item_table, item_pool_weights from .Locations import RiskOfRainLocation, item_pickups -from .Options import ItemWeights, ror2_options +from .Options import ItemWeights, ROR2Options from .Rules import set_rules from BaseClasses import Region, RegionType, Entrance, Item, ItemClassification, MultiWorld, Tutorial @@ -49,7 +49,7 @@ def generate_early(self) -> None: def generate_basic(self) -> None: # shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend if self.o.start_with_revive.value: - self.world.push_precollected(self.world.create_item("Dio's Best Friend", self.player)) + self.multiworld.push_precollected(self.create_item("Dio's Best Friend")) # if presets are enabled generate junk_pool from the selected preset pool_option = self.o.item_weights.value @@ -135,7 +135,7 @@ def create_events(self) -> None: num_of_events = total_locations // 25 if total_locations / 25 == num_of_events: num_of_events -= 1 - world_region = self.world.get_region("Petrichor V", self.player) + world_region = self.multiworld.get_region("Petrichor V", self.player) for i in range(num_of_events): event_loc = RiskOfRainLocation(self.player, f"Pickup{(i + 1) * 25}", None, world_region) From d85fb695add1a0d30c06e5b5cf039f0e1c270c79 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 12 Feb 2023 12:23:16 -0600 Subject: [PATCH 15/88] have current option updates use `.value` instead of changing the option --- worlds/factorio/__init__.py | 4 ++-- worlds/oot/__init__.py | 4 ++-- worlds/sm/__init__.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 0053a016e372..f58be1b1758e 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -73,8 +73,8 @@ def __init__(self, world, player: int): generate_output = generate_mod def generate_early(self) -> None: - self.multiworld.max_tech_cost[self.player] = max(self.multiworld.max_tech_cost[self.player], - self.multiworld.min_tech_cost[self.player]) + self.multiworld.max_tech_cost[self.player].value = max(self.multiworld.max_tech_cost[self.player].value, + self.multiworld.min_tech_cost[self.player].value) self.tech_mix = self.multiworld.tech_cost_mix[self.player] self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 815182655d33..f594e4c6eb2a 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -30,7 +30,7 @@ from Utils import get_options from BaseClasses import MultiWorld, CollectionState, RegionType, 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 @@ -240,7 +240,7 @@ def generate_early(self): # 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) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index c7f41092f596..63ff651e8982 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -129,7 +129,7 @@ def generate_early(self): 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 generate_basic(self): From cfdc0b1fa34647056d3cc81659d4e1b50303afe7 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 12 Feb 2023 12:55:34 -0600 Subject: [PATCH 16/88] update ror2 to use the new options system again --- BaseClasses.py | 4 +- Options.py | 6 ++- worlds/ror2/Options.py | 3 +- worlds/ror2/__init__.py | 103 ++++++++++++++++++---------------------- 4 files changed, 54 insertions(+), 62 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index f062ed6018bf..f3e97d570b97 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -238,9 +238,9 @@ def set_options(self, args: Namespace) -> None: option_values = getattr(args, option_key, {}) setattr(self, option_key, option_values) # TODO - remove this loop once all worlds use options dataclasses - options_dataclass: Type[Options.GameOptions] = self.worlds[player].options_dataclass + options_dataclass: typing.Type[Options.GameOptions] = self.worlds[player].options_dataclass self.worlds[player].o = options_dataclass(**{option_key: getattr(args, option_key)[player] - for option_key in get_type_hints(options_dataclass)}) + for option_key in typing.get_type_hints(options_dataclass)}) def set_item_links(self): item_links = {} diff --git a/Options.py b/Options.py index 2c69e7b0656d..31956b766733 100644 --- a/Options.py +++ b/Options.py @@ -866,7 +866,11 @@ class CommonOptions: progression_balancing: ProgressionBalancing accessibility: Accessibility - def as_dict(self, *args: str) -> typing.Dict: + def as_dict(self, *args: str) -> typing.Dict[str, typing.Any]: + """ + Pass the option_names you would like returned as a dictionary as strings. + Returns a dictionary of [str, Option.value] + """ option_results = {} for option_name in args: if option_name in self.__annotations__: diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py index f96fa61cf17d..84a3c92ac347 100644 --- a/worlds/ror2/Options.py +++ b/worlds/ror2/Options.py @@ -1,6 +1,5 @@ -from typing import Dict from dataclasses import dataclass -from Options import Option, Toggle, DefaultOnToggle, DeathLink, Range, Choice +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 diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 7f8a7ce6a9fd..ed7a2778b0cf 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -1,4 +1,5 @@ import string +from typing import get_type_hints from .Items import RiskOfRainItem, item_table, item_pool_weights, environment_offest from .Locations import RiskOfRainLocation, get_classic_item_pickups, item_pickups, orderedstage_location @@ -6,7 +7,7 @@ from .RoR2Environments import * from BaseClasses import Region, RegionType, 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,7 +29,7 @@ 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" + game = "Risk of Rain 2" option_definitions = get_type_hints(ROR2Options) options_dataclass = ROR2Options o: ROR2Options @@ -44,45 +45,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.o.goal == "classic": + total_locations = self.o.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.o.chests_per_stage.value, + shrines=self.o.shrines_per_stage.value, + scavengers=self.o.scavengers_per_stage.value, + scanners=self.o.scanner_per_stage.value, + altars=self.o.altars_per_stage.value, + dlc_sotv=self.o.dlc_sotv.value ) ) - self.total_revivals = int(self.multiworld.total_revivals[self.player].value / 100 * + self.total_revivals = int(self.o.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.o.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.o.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.o.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.o.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.o.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.o.begin_with_loop.value 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,25 +100,15 @@ def create_items(self) -> None: else: 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 - } + junk_pool = self.o.as_dict("Item Scrap, Green", "Item Scrap, Red", "Item Scrap, Yellow", "Item Scrap, White", + "Common Item", "Uncommon Item", "Legendary Item", "Boss Item", "Lunar Item", + "Void Item", "Equipment") # 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.o.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.o.dlc_sotv or pool_option == ItemWeights.option_void: junk_pool.pop("Void Item") # Generate item pool @@ -131,19 +121,19 @@ def create_items(self) -> None: # precollected environments are popped from the pool so counting like this is valid nonjunk_item_count = self.total_revivals + len(environments_pool) - if self.multiworld.goal[self.player] == "classic": + if self.o.goal == "classic": # classic mode - total_locations = self.multiworld.total_locations[self.player].value + total_locations = self.o.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.o.chests_per_stage.value, + shrines=self.o.shrines_per_stage.value, + scavengers=self.o.scavengers_per_stage.value, + scanners=self.o.scanner_per_stage.value, + altars=self.o.altars_per_stage.value, + dlc_sotv=self.o.dlc_sotv.value ) ) junk_item_count = total_locations - nonjunk_item_count @@ -160,7 +150,7 @@ def set_rules(self) -> None: def create_regions(self) -> None: - if self.multiworld.goal[self.player] == "classic": + if self.o.goal == "classic": # classic mode menu = create_region(self.multiworld, self.player, "Menu") self.multiworld.regions.append(menu) @@ -169,7 +159,7 @@ def create_regions(self) -> None: 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.o.total_locations.value)) self.multiworld.regions.append(petrichor) # classic mode can get to victory from the beginning of the game @@ -184,24 +174,23 @@ def create_regions(self) -> None: # explore mode create_regions(self.multiworld, self.player) - self.create_events() + create_events(self.multiworld, self.player) def fill_slot_data(self): + options_dict = self.o.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") + cased_dict = {} + for key, value in options_dict.items(): + split_name = [name.title() for name in key.split("_")] + split_name[0] = split_name[0].lower() + new_name = "".join(split_name) + cased_dict[new_name] = value + 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, + **cased_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: From eaadb6ecc135107b0fc1e5a4bcf36598e73e845e Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 12 Feb 2023 12:59:05 -0600 Subject: [PATCH 17/88] revert the junk pool dict since it's cased differently --- worlds/ror2/__init__.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index ed7a2778b0cf..caf4cf54c967 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -100,9 +100,19 @@ def create_items(self) -> None: else: junk_pool = item_pool_weights[pool_option].copy() else: # generate junk pool from user created presets - junk_pool = self.o.as_dict("Item Scrap, Green", "Item Scrap, Red", "Item Scrap, Yellow", "Item Scrap, White", - "Common Item", "Uncommon Item", "Legendary Item", "Boss Item", "Lunar Item", - "Void Item", "Equipment") + junk_pool = { + "Item Scrap, Green": self.o.green_scrap.value, + "Item Scrap, Red": self.o.red_scrap.value, + "Item Scrap, Yellow": self.o.yellow_scrap.value, + "Item Scrap, White": self.o.white_scrap.value, + "Common Item": self.o.common_item.value, + "Uncommon Item": self.o.uncommon_item.value, + "Legendary Item": self.o.legendary_item.value, + "Boss Item": self.o.boss_item.value, + "Lunar Item": self.o.lunar_item.value, + "Void Item": self.o.void_item.value, + "Equipment": self.o.equipment.value + } # remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled if not self.o.enable_lunar or pool_option == ItemWeights.option_lunartic: From 0658a5b2f21ebd37487936ad65a3428810988801 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 13 Feb 2023 19:34:06 -0600 Subject: [PATCH 18/88] fix begin_with_loop typo --- worlds/ror2/Options.py | 2 +- worlds/ror2/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py index 84a3c92ac347..caa024d100bd 100644 --- a/worlds/ror2/Options.py +++ b/worlds/ror2/Options.py @@ -300,7 +300,7 @@ class ROR2Options(PerGameCommonOptions, ROR2Weights): total_revivals: TotalRevivals start_with_revive: StartWithRevive final_stage_death: FinalStageDeath - being_with_loop: BeginWithLoop + begin_with_loop: BeginWithLoop dlc_sotv: DLC_SOTV death_link: DeathLink item_pickup_step: ItemPickupStep diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 8306eb693704..4ee137968ea0 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -82,7 +82,7 @@ def create_items(self) -> None: if self.o.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.o.begin_with_loop.value else 1 + environments_to_precollect = 5 if self.o.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) From effbca039bf78c67e68d6fc0fefe2bb50e0ad303 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 14 Feb 2023 12:34:24 -0600 Subject: [PATCH 19/88] write new and old options to spoiler --- BaseClasses.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 85d49daf0679..1ed35e0b7a13 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,5 +1,6 @@ from __future__ import annotations +import collections import copy import itertools import functools @@ -1573,8 +1574,11 @@ def bool_to_text(variable: Union[bool, str]) -> str: return variable return 'Yes' if variable else 'No' - def write_option(option_key: str, option_obj: type(Options.Option)): - res = getattr(self.multiworld, option_key)[player] + def write_option(option_key: str, option_obj: Union[type(Options.Option), "Options.AssembleOptions"]): + if hasattr(self.multiworld.worlds[player].o, option_key): + res = getattr(self.multiworld.worlds[player].o, option_key) + else: # TODO remove when all worlds move to new system + res = getattr(self.multiworld, option_key)[player] display_name = getattr(option_obj, "display_name", option_key) try: outfile.write(f'{display_name + ":":33}{res.get_current_option_name()}\n') @@ -1594,12 +1598,16 @@ def write_option(option_key: str, option_obj: type(Options.Option)): if self.multiworld.players > 1: outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player))) outfile.write('Game: %s\n' % self.multiworld.game[player]) - for f_option, option in Options.per_game_common_options.items(): - write_option(f_option, option) - options = self.multiworld.worlds[player].option_definitions - if options: - for f_option, option in options.items(): + + if self.multiworld.worlds[player].o is not Options.PerGameCommonOptions: + for f_option, option in self.multiworld.worlds[player].o.__annotations__.items(): + write_option(f_option, option) + else: # TODO remove when all worlds move to new system + options = self.multiworld.worlds[player].option_definitions + for f_option, option\ + in collections.ChainMap(Options.PerGameCommonOptions.__annotations__.items(), options): write_option(f_option, option) + AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile) if player in self.multiworld.get_game_players("A Link to the Past"): From b78b7d383bb7c725e1401e2d352f9db5d17ca6d0 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 14 Feb 2023 12:35:14 -0600 Subject: [PATCH 20/88] change factorio option behavior back --- worlds/factorio/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 03c997cc2c93..e691ac61c908 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -73,8 +73,8 @@ def __init__(self, world, player: int): generate_output = generate_mod def generate_early(self) -> None: - self.multiworld.max_tech_cost[self.player].value = max(self.multiworld.max_tech_cost[self.player].value, - self.multiworld.min_tech_cost[self.player].value) + self.multiworld.max_tech_cost[self.player] = max(self.multiworld.max_tech_cost[self.player], + self.multiworld.min_tech_cost[self.player]) self.tech_mix = self.multiworld.tech_cost_mix[self.player] self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn From 94d18dc82c67aefafceac7d1c37807d0683220e7 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 14 Feb 2023 13:24:48 -0600 Subject: [PATCH 21/88] fix comparisons --- BaseClasses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 1ed35e0b7a13..c9193a5d807d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1599,13 +1599,13 @@ def write_option(option_key: str, option_obj: Union[type(Options.Option), "Optio outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player))) outfile.write('Game: %s\n' % self.multiworld.game[player]) - if self.multiworld.worlds[player].o is not Options.PerGameCommonOptions: + if type(self.multiworld.worlds[player].o) is not Options.PerGameCommonOptions: for f_option, option in self.multiworld.worlds[player].o.__annotations__.items(): write_option(f_option, option) else: # TODO remove when all worlds move to new system options = self.multiworld.worlds[player].option_definitions for f_option, option\ - in collections.ChainMap(Options.PerGameCommonOptions.__annotations__.items(), options): + in collections.ChainMap(Options.PerGameCommonOptions.__annotations__, options).items(): write_option(f_option, option) AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile) From 0c0663bc46e1ec1d06805d05735102f9172d2e31 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 14 Feb 2023 13:39:56 -0600 Subject: [PATCH 22/88] move common and per_game_common options to new system --- BaseClasses.py | 2 +- Fill.py | 8 ++++---- Main.py | 22 +++++++++++----------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index c9193a5d807d..6575178311ec 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -247,7 +247,7 @@ 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].o.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']}") diff --git a/Fill.py b/Fill.py index 725d1a41d090..7ee5b5be5999 100644 --- a/Fill.py +++ b/Fill.py @@ -221,7 +221,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].o.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: @@ -244,7 +244,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].o.accessibility != 'minimal') for location in unreachable_locations: add_item_rule(location, forbid_important_item_rule) @@ -487,9 +487,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].o.progression_balancing / 100 for player in world.player_ids - if world.progression_balancing[player] > 0 + if world.worlds[player].o.progression_balancing > 0 } if not balanceable_players: logging.info('Skipping multiworld progression balancing.') diff --git a/Main.py b/Main.py index 04a7e3bff60b..7fa88ed8aa3f 100644 --- a/Main.py +++ b/Main.py @@ -113,7 +113,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].o.start_inventory.value.items(): for _ in range(count): world.push_precollected(world.create_item(item_name, player)) @@ -131,21 +131,21 @@ 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].o.non_local_items.value -= world.worlds[player].o.local_items.value + world.worlds[player].o.non_local_items.value -= set(world.local_early_items[player]) if world.players > 1: locality_rules(world) else: - world.non_local_items[1].value = set() - world.local_items[1].value = set() + world.worlds[1].o.non_local_items.value = set() + world.worlds[1].o.local_items.value = set() 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].o.exclude_locations.value) + world.worlds[player].o.priority_locations.value -= world.worlds[player].o.exclude_locations.value + for location_name in world.worlds[player].o.priority_locations.value: world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY AutoWorld.call_all(world, "generate_basic") @@ -348,11 +348,11 @@ def precollect_hint(location): 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].o.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].o.start_hints: precollect_hint(location) - elif any([location.item.name in world.start_hints[player] + elif any([location.item.name in world.worlds[player].o.start_hints for player in world.groups.get(location.item.player, {}).get("players", [])]): precollect_hint(location) From a6385827ec3423f0d286d2076cdd65e2562d1733 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Tue, 14 Feb 2023 22:55:56 +0100 Subject: [PATCH 23/88] core: automatically create missing options_dataclass from legacy option_definitions --- worlds/AutoWorld.py | 7 +++++++ worlds/alttp/test/inverted/TestInvertedBombRules.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 2af3969061d3..bff9e7cf2bc8 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -3,6 +3,7 @@ import logging import sys import pathlib +from dataclasses import make_dataclass from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Type, Union, TYPE_CHECKING, \ ClassVar @@ -44,6 +45,12 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Aut 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: diff --git a/worlds/alttp/test/inverted/TestInvertedBombRules.py b/worlds/alttp/test/inverted/TestInvertedBombRules.py index 1bb8a79be767..c3bdb5ffd468 100644 --- a/worlds/alttp/test/inverted/TestInvertedBombRules.py +++ b/worlds/alttp/test/inverted/TestInvertedBombRules.py @@ -19,7 +19,7 @@ def setUp(self): args = self.multiworld.default_common_options 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_options(args) self.multiworld.difficulty_requirements[1] = difficulties['normal'] create_inverted_regions(self.multiworld, 1) create_dungeons(self.multiworld, 1) From fe679fce154eae0dfb9d3d0c6296c0f006dc5798 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 14 Feb 2023 17:21:45 -0600 Subject: [PATCH 24/88] remove spoiler special casing and add back the Factorio option changing but in new system --- BaseClasses.py | 15 +++------------ worlds/factorio/__init__.py | 7 +++---- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 6575178311ec..b0b4118fd0da 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1575,10 +1575,7 @@ def bool_to_text(variable: Union[bool, str]) -> str: return 'Yes' if variable else 'No' def write_option(option_key: str, option_obj: Union[type(Options.Option), "Options.AssembleOptions"]): - if hasattr(self.multiworld.worlds[player].o, option_key): - res = getattr(self.multiworld.worlds[player].o, option_key) - else: # TODO remove when all worlds move to new system - res = getattr(self.multiworld, option_key)[player] + res = getattr(self.multiworld.worlds[player].o, option_key) display_name = getattr(option_obj, "display_name", option_key) try: outfile.write(f'{display_name + ":":33}{res.get_current_option_name()}\n') @@ -1599,14 +1596,8 @@ def write_option(option_key: str, option_obj: Union[type(Options.Option), "Optio outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player))) outfile.write('Game: %s\n' % self.multiworld.game[player]) - if type(self.multiworld.worlds[player].o) is not Options.PerGameCommonOptions: - for f_option, option in self.multiworld.worlds[player].o.__annotations__.items(): - write_option(f_option, option) - else: # TODO remove when all worlds move to new system - options = self.multiworld.worlds[player].option_definitions - for f_option, option\ - in collections.ChainMap(Options.PerGameCommonOptions.__annotations__, options).items(): - write_option(f_option, option) + for f_option, option in self.multiworld.worlds[player].o.__annotations__.items(): + write_option(f_option, option) AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index e691ac61c908..a25e084819b8 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -73,10 +73,9 @@ def __init__(self, world, player: int): generate_output = generate_mod def generate_early(self) -> None: - self.multiworld.max_tech_cost[self.player] = max(self.multiworld.max_tech_cost[self.player], - self.multiworld.min_tech_cost[self.player]) - self.tech_mix = self.multiworld.tech_cost_mix[self.player] - self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn + self.o.max_tech_cost = max(self.o.max_tech_cost, self.o.min_tech_cost) + self.tech_mix = self.o.tech_cost_mix + self.skip_silo = self.o.silo.value == Silo.option_spawn def create_regions(self): player = self.player From c5684bb3b628e221245c0f3cbb6d259e739c3fb5 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 14 Feb 2023 19:29:32 -0600 Subject: [PATCH 25/88] give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly --- worlds/archipidle/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/archipidle/__init__.py b/worlds/archipidle/__init__.py index 5054872dbec3..77d9222d0c45 100644 --- a/worlds/archipidle/__init__.py +++ b/worlds/archipidle/__init__.py @@ -1,4 +1,5 @@ from BaseClasses import Item, MultiWorld, Region, Location, Entrance, Tutorial, ItemClassification +from Options import PerGameCommonOptions from .Items import item_table from .Rules import set_rules from ..AutoWorld import World, WebWorld @@ -29,6 +30,8 @@ class ArchipIDLEWorld(World): hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April web = ArchipIDLEWebWorld() + options_dataclass = PerGameCommonOptions + item_name_to_id = {} start_id = 9000 for item in item_table: From cecd3f725b263051d38694a488592e2c91bf8400 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 16 Feb 2023 23:22:04 -0600 Subject: [PATCH 26/88] reimplement `inspect.get_annotations` --- Utils.py | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/Utils.py b/Utils.py index 133f1c452e06..beccf1336f4b 100644 --- a/Utils.py +++ b/Utils.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import types import typing import builtins import os @@ -682,6 +683,75 @@ def read_snes_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray: _faf_tasks: "Set[asyncio.Task[None]]" = set() +def get_annotations(obj, *, globals=None, locals=None, eval_str=False): + """ + Shamelessly copy pasted implementation of `inspect.get_annotations` from Python 3.10. + TODO remove this once Python 3.8 and 3.9 support is dropped and replace with `inspect.get_annotations` + """ + if isinstance(obj, type): + # class + obj_dict = getattr(obj, "__dict__", None) + if obj_dict and hasattr(obj_dict, "get"): + ann = obj_dict.get("__annotations__", None) + if isinstance(ann, types.GetSetDescriptorType): + ann = None + else: + ann = None + + obj_globals = None + module_name = getattr(obj, "__module__", None) + if module_name: + module = sys.modules.get(module_name, None) + if module_name: + module = sys.modules.get(module_name, None) + if module: + obj_globals = getattr(module, "__dict__", None) + obj_locals = dict(vars(obj)) + unwrap = obj + elif isinstance(obj, types.ModuleType): + # module + ann = getattr(obj, "__annotations__", None) + obj_globals = getattr(obj, "__dict__") + obj_locals = None + unwrap = None + elif callable(obj): + ann = getattr(obj, "__annotations__", None) + obj_globals = getattr(obj, "__globals__", None) + obj_locals = None + unwrap = obj + else: + raise TypeError(f"{obj!r} is not a module, class, or callable") + + if ann is None: + return {} + if not isinstance(ann, dict): + raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None") + if not ann: + return {} + if not eval_str: + return dict(ann) + if unwrap is not None: + while True: + if hasattr(unwrap, "__wrapped__"): + unwrap = unwrap.__wrapped__ + continue + if isinstance(unwrap, functools.partial): + unwrap = unwrap.func + continue + break + if hasattr(unwrap, "__globals__"): + obj_globals = unwrap.__globals__ + + if globals is None: + globals = obj_globals + if locals is None: + locals = obj_locals + + return_value = {key: value if not isinstance(value, str) else eval(value, globals, locals) + for key, value in ann.items()} + return return_value + + def async_start(co: Coroutine[typing.Any, typing.Any, bool], name: Optional[str] = None) -> None: """ Use this to start a task when you don't keep a reference to it or immediately await it, From 1fbc1a4f32bc798a4aa350690cb2f8ef9a85a350 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 16 Feb 2023 23:22:50 -0600 Subject: [PATCH 27/88] move option info generation for webhost to new system --- WebHostLib/options.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 8f366d4fbf31..a5d56780398c 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -7,7 +7,7 @@ from jinja2 import Template import Options -from Utils import __version__, local_path +from Utils import __version__, local_path, get_annotations from worlds.AutoWorld import AutoWorldRegister handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints", @@ -58,8 +58,8 @@ def get_html_doc(option_type: type(Options.Option)) -> str: for game_name, world in AutoWorldRegister.world_types.items(): all_options: typing.Dict[str, Options.AssembleOptions] = { - **Options.per_game_common_options, - **world.option_definitions + **get_annotations(Options.PerGameCommonOptions, eval_str=True), + **get_annotations(world.options_dataclass, eval_str=True) } with open(local_path("WebHostLib", "templates", "options.yaml")) as f: file_data = f.read() From c3ad00b8d9e4a9085b38ae4a75c8e001c20abc8f Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 16 Feb 2023 23:30:02 -0600 Subject: [PATCH 28/88] need to include Common and PerGame common since __annotations__ doesn't include super --- WebHostLib/options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index a5d56780398c..346bd1b3b624 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -58,6 +58,7 @@ def get_html_doc(option_type: type(Options.Option)) -> str: for game_name, world in AutoWorldRegister.world_types.items(): all_options: typing.Dict[str, Options.AssembleOptions] = { + **get_annotations(Options.CommonOptions, eval_str=True), **get_annotations(Options.PerGameCommonOptions, eval_str=True), **get_annotations(world.options_dataclass, eval_str=True) } From 1b1ee314c80605fd775b362a0709080a77a8f122 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 21 Feb 2023 08:47:43 -0600 Subject: [PATCH 29/88] use get_type_hints for the options dictionary --- Utils.py | 69 ------------------------------------------- WebHostLib/options.py | 8 ++--- 2 files changed, 4 insertions(+), 73 deletions(-) diff --git a/Utils.py b/Utils.py index beccf1336f4b..777c3283935c 100644 --- a/Utils.py +++ b/Utils.py @@ -683,75 +683,6 @@ def read_snes_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray: _faf_tasks: "Set[asyncio.Task[None]]" = set() -def get_annotations(obj, *, globals=None, locals=None, eval_str=False): - """ - Shamelessly copy pasted implementation of `inspect.get_annotations` from Python 3.10. - TODO remove this once Python 3.8 and 3.9 support is dropped and replace with `inspect.get_annotations` - """ - if isinstance(obj, type): - # class - obj_dict = getattr(obj, "__dict__", None) - if obj_dict and hasattr(obj_dict, "get"): - ann = obj_dict.get("__annotations__", None) - if isinstance(ann, types.GetSetDescriptorType): - ann = None - else: - ann = None - - obj_globals = None - module_name = getattr(obj, "__module__", None) - if module_name: - module = sys.modules.get(module_name, None) - if module_name: - module = sys.modules.get(module_name, None) - if module: - obj_globals = getattr(module, "__dict__", None) - obj_locals = dict(vars(obj)) - unwrap = obj - elif isinstance(obj, types.ModuleType): - # module - ann = getattr(obj, "__annotations__", None) - obj_globals = getattr(obj, "__dict__") - obj_locals = None - unwrap = None - elif callable(obj): - ann = getattr(obj, "__annotations__", None) - obj_globals = getattr(obj, "__globals__", None) - obj_locals = None - unwrap = obj - else: - raise TypeError(f"{obj!r} is not a module, class, or callable") - - if ann is None: - return {} - if not isinstance(ann, dict): - raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None") - if not ann: - return {} - if not eval_str: - return dict(ann) - if unwrap is not None: - while True: - if hasattr(unwrap, "__wrapped__"): - unwrap = unwrap.__wrapped__ - continue - if isinstance(unwrap, functools.partial): - unwrap = unwrap.func - continue - break - if hasattr(unwrap, "__globals__"): - obj_globals = unwrap.__globals__ - - if globals is None: - globals = obj_globals - if locals is None: - locals = obj_locals - - return_value = {key: value if not isinstance(value, str) else eval(value, globals, locals) - for key, value in ann.items()} - return return_value - - def async_start(co: Coroutine[typing.Any, typing.Any, bool], name: Optional[str] = None) -> None: """ Use this to start a task when you don't keep a reference to it or immediately await it, diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 346bd1b3b624..b2ce6df2de50 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -7,7 +7,7 @@ from jinja2 import Template import Options -from Utils import __version__, local_path, get_annotations +from Utils import __version__, local_path from worlds.AutoWorld import AutoWorldRegister handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints", @@ -58,9 +58,9 @@ def get_html_doc(option_type: type(Options.Option)) -> str: for game_name, world in AutoWorldRegister.world_types.items(): all_options: typing.Dict[str, Options.AssembleOptions] = { - **get_annotations(Options.CommonOptions, eval_str=True), - **get_annotations(Options.PerGameCommonOptions, eval_str=True), - **get_annotations(world.options_dataclass, eval_str=True) + **typing.get_type_hints(Options.CommonOptions), + **typing.get_type_hints(Options.PerGameCommonOptions), + **typing.get_type_hints(world.options_dataclass) } with open(local_path("WebHostLib", "templates", "options.yaml")) as f: file_data = f.read() From 85e98a0471a2c38494499901a484c1788a987471 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 21 Feb 2023 08:49:36 -0600 Subject: [PATCH 30/88] typing.get_type_hints returns the bases too. --- WebHostLib/options.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index b2ce6df2de50..117f57549b63 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -57,11 +57,8 @@ def get_html_doc(option_type: type(Options.Option)) -> str: for game_name, world in AutoWorldRegister.world_types.items(): - all_options: typing.Dict[str, Options.AssembleOptions] = { - **typing.get_type_hints(Options.CommonOptions), - **typing.get_type_hints(Options.PerGameCommonOptions), - **typing.get_type_hints(world.options_dataclass) - } + all_options: typing.Dict[str, Options.AssembleOptions] = typing.get_type_hints(world.options_dataclass) + with open(local_path("WebHostLib", "templates", "options.yaml")) as f: file_data = f.read() res = Template(file_data).render( From a24bb2e5d8d60b6ea5f109f5ae70ba31630b32d6 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 21 Feb 2023 09:34:26 -0600 Subject: [PATCH 31/88] forgot to sweep through generate --- Generate.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Generate.py b/Generate.py index dadabd7ac6ec..46784418ac1a 100644 --- a/Generate.py +++ b/Generate.py @@ -8,7 +8,7 @@ import urllib.parse import urllib.request from collections import Counter, ChainMap -from typing import Dict, Tuple, Callable, Any, Union +from typing import Dict, Tuple, Callable, Any, Union, get_type_hints import ModuleUpdate @@ -339,7 +339,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 = get_type_hints(game_world.options_dataclass) if option_key in options: if options[option_key].supports_weighting: return get_choice(option_key, category_dict) @@ -464,13 +464,8 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default))) if ret.game in AutoWorldRegister.world_types: - for option_key, option in world_type.option_definitions.items(): + for option_key, option in get_type_hints(world_type.options_dataclass).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": From 9df025daeb3936cf0349bb0cf42d0bb13b0a43d9 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 21 Feb 2023 09:34:38 -0600 Subject: [PATCH 32/88] sweep through all the tests --- BaseClasses.py | 12 +--------- test/TestBase.py | 5 +++-- test/general/TestFill.py | 18 ++++++++------- test/general/__init__.py | 8 ++++--- worlds/alttp/test/__init__.py | 16 ++++++++++++++ worlds/alttp/test/dungeons/TestDungeon.py | 16 ++++---------- worlds/alttp/test/inverted/TestInverted.py | 14 ++++-------- .../test/inverted/TestInvertedBombRules.py | 14 +++--------- .../TestInvertedMinor.py | 17 +++++--------- .../test/inverted_owg/TestInvertedOWG.py | 16 ++++---------- worlds/alttp/test/minor_glitches/TestMinor.py | 18 ++++----------- worlds/alttp/test/owg/TestVanillaOWG.py | 21 +++++------------- worlds/alttp/test/vanilla/TestVanilla.py | 22 +++++-------------- 13 files changed, 70 insertions(+), 127 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b0b4118fd0da..10ad846432e1 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -234,8 +234,7 @@ def set_options(self, args: Namespace) -> None: self.custom_data[player] = {} world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] self.worlds[player] = world_type(self, player) - for option_key in itertools.chain(Options.common_options, Options.per_game_common_options, - world_type.option_definitions): + for option_key in typing.get_type_hints(world_type.options_dataclass): option_values = getattr(args, option_key, {}) setattr(self, option_key, option_values) # TODO - remove this loop once all worlds use options dataclasses @@ -302,15 +301,6 @@ def set_item_links(self): group["non_local_items"] = item_link["non_local_items"] group["link_replacement"] = replacement_prio[item_link["link_replacement"]] - # intended for unittests - @property - def default_common_options(self) -> Namespace: - self.state = CollectionState(self) - args = Namespace() - for option_key, option in itertools.chain(Options.common_options.items(), Options.per_game_common_options.items()): - setattr(args, option_key, {player_id: option.from_any(option.default) for player_id in self.player_ids}) - return args - def secure(self): self.random = ThreadBarrierProxy(secrets.SystemRandom()) self.is_race = True diff --git a/test/TestBase.py b/test/TestBase.py index c4902d4799ce..220ecc929909 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -118,8 +118,9 @@ def world_setup(self, seed: typing.Optional[int] = None) -> None: self.multiworld.game[1] = self.game self.multiworld.player_name = {1: "Tester"} self.multiworld.set_seed(seed) - args = self.multiworld.default_common_options - for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].option_definitions.items(): + self.multiworld.state = CollectionState(self.multiworld) + args = Namespace() + for name, option in typing.get_type_hints(AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass).items(): setattr(args, name, { 1: option.from_any(self.options.get(name, getattr(option, "default"))) }) diff --git a/test/general/TestFill.py b/test/general/TestFill.py index 927267521f8a..5b9b1d9acaa8 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -1,4 +1,5 @@ import itertools +from argparse import Namespace from typing import get_type_hints, List, Iterable import unittest @@ -8,14 +9,14 @@ 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 = {} - args = multi_world.default_common_options + multi_world.state = CollectionState(multi_world) for i in range(players): player_id = i+1 world = World(multi_world, player_id) @@ -24,12 +25,13 @@ 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 in itertools.chain(Options.common_options, Options.per_game_common_options): - option_value = getattr(args, option_key, {}) - setattr(multi_world, option_key, option_value) - # TODO - remove this loop once all worlds use options dataclasses - world.o = world.options_dataclass(**{option_key: getattr(args, option_key)[player_id] + for option_key, option in get_type_hints(Options.PerGameCommonOptions).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.o = world.options_dataclass(**{option_key: getattr(multi_world, option_key)[player_id] for option_key in get_type_hints(world.options_dataclass)}) multi_world.set_seed(0) diff --git a/test/general/__init__.py b/test/general/__init__.py index f985989dbba7..b6bede376fd7 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -1,6 +1,7 @@ from argparse import Namespace +from typing import get_type_hints -from BaseClasses import MultiWorld +from BaseClasses import MultiWorld, CollectionState from worlds.AutoWorld import call_all gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"] @@ -11,8 +12,9 @@ def setup_solo_multiworld(world_type) -> MultiWorld: multiworld.game[1] = world_type.game multiworld.player_name = {1: "Tester"} multiworld.set_seed() - args = multiworld.default_common_options - for name, option in world_type.option_definitions.items(): + multiworld.state = CollectionState(multiworld) + args = Namespace() + for name, option in get_type_hints(world_type.options_dataclass).items(): setattr(args, name, {1: option.from_any(option.default)}) multiworld.set_options(args) for step in gen_steps: diff --git a/worlds/alttp/test/__init__.py b/worlds/alttp/test/__init__.py index e69de29bb2d1..4efc3723ea5e 100644 --- a/worlds/alttp/test/__init__.py +++ b/worlds/alttp/test/__init__.py @@ -0,0 +1,16 @@ +import unittest +from argparse import Namespace +from typing import get_type_hints + +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) + args = Namespace() + for name, option in get_type_hints(AutoWorldRegister.world_types["A Link to the Past"].options_dataclass).items(): + setattr(args, name, {1: option.from_any(getattr(option, "default"))}) + self.multiworld.set_options(args) diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py index 9641f2ce8ec4..7397c02100af 100644 --- a/worlds/alttp/test/dungeons/TestDungeon.py +++ b/worlds/alttp/test/dungeons/TestDungeon.py @@ -1,24 +1,16 @@ -import unittest -from argparse import Namespace - -from BaseClasses import MultiWorld, CollectionState, ItemClassification +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, generate_itempool from worlds.alttp.Items import ItemFactory from worlds.alttp.Regions import create_regions from worlds.alttp.Shops import create_shops -from worlds.alttp.Rules import set_rules -from worlds import AutoWorld +from worlds.alttp.test import LTTPTestBase -class TestDungeon(unittest.TestCase): +class TestDungeon(LTTPTestBase): def setUp(self): - self.multiworld = MultiWorld(1) - args = self.multiworld.default_common_options - 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.world_setup() self.starting_regions = [] # Where to start exploring self.remove_exits = [] # Block dungeon exits self.multiworld.difficulty_requirements[1] = difficulties['normal'] diff --git a/worlds/alttp/test/inverted/TestInverted.py b/worlds/alttp/test/inverted/TestInverted.py index 95de61da648e..7d675d9dccb6 100644 --- a/worlds/alttp/test/inverted/TestInverted.py +++ b/worlds/alttp/test/inverted/TestInverted.py @@ -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,15 +7,12 @@ 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) - args = self.multiworld.default_common_options - 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.world_setup() self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.mode[1] = "inverted" create_inverted_regions(self.multiworld, 1) diff --git a/worlds/alttp/test/inverted/TestInvertedBombRules.py b/worlds/alttp/test/inverted/TestInvertedBombRules.py index c3bdb5ffd468..df31dafecc7e 100644 --- a/worlds/alttp/test/inverted/TestInvertedBombRules.py +++ b/worlds/alttp/test/inverted/TestInvertedBombRules.py @@ -1,25 +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.world_setup() self.multiworld.mode[1] = "inverted" - args = self.multiworld.default_common_options - 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.difficulty_requirements[1] = difficulties['normal'] create_inverted_regions(self.multiworld, 1) create_dungeons(self.multiworld, 1) diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py index f9068da60a8e..1368b063a22f 100644 --- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py +++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py @@ -1,25 +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) - args = self.multiworld.default_common_options - 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.world_setup() self.multiworld.mode[1] = "inverted" self.multiworld.logic[1] = "minorglitches" self.multiworld.difficulty_requirements[1] = difficulties['normal'] diff --git a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py index 78f008680a55..77698b863a09 100644 --- a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py +++ b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py @@ -1,26 +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) - args = self.multiworld.default_common_options - 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.world_setup() self.multiworld.logic[1] = "owglitches" self.multiworld.mode[1] = "inverted" self.multiworld.difficulty_requirements[1] = difficulties['normal'] diff --git a/worlds/alttp/test/minor_glitches/TestMinor.py b/worlds/alttp/test/minor_glitches/TestMinor.py index a0a87e913eeb..d5cfd3095b9c 100644 --- a/worlds/alttp/test/minor_glitches/TestMinor.py +++ b/worlds/alttp/test/minor_glitches/TestMinor.py @@ -1,25 +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) - args = self.multiworld.default_common_options - 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.world_setup() self.multiworld.logic[1] = "minorglitches" self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.worlds[1].er_seed = 0 diff --git a/worlds/alttp/test/owg/TestVanillaOWG.py b/worlds/alttp/test/owg/TestVanillaOWG.py index fa288ea5b44e..37b0b6ccb868 100644 --- a/worlds/alttp/test/owg/TestVanillaOWG.py +++ b/worlds/alttp/test/owg/TestVanillaOWG.py @@ -1,26 +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, generate_itempool +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.alttp.Rules import set_rules 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) - args = self.multiworld.default_common_options - 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.world_setup() self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.logic[1] = "owglitches" self.multiworld.worlds[1].er_seed = 0 diff --git a/worlds/alttp/test/vanilla/TestVanilla.py b/worlds/alttp/test/vanilla/TestVanilla.py index c2352ebb20e5..3c983e98504c 100644 --- a/worlds/alttp/test/vanilla/TestVanilla.py +++ b/worlds/alttp/test/vanilla/TestVanilla.py @@ -1,24 +1,14 @@ -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, generate_itempool +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.alttp.Rules import set_rules 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) - args = self.multiworld.default_common_options - 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.world_setup() self.multiworld.logic[1] = "noglitches" self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.worlds[1].er_seed = 0 From 337133c61174529e02750446088bdb3e2117da92 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Wed, 22 Feb 2023 16:46:04 -0600 Subject: [PATCH 33/88] swap to a metaclass property --- BaseClasses.py | 2 +- Generate.py | 8 ++++---- Options.py | 9 ++++++++- WebHostLib/options.py | 2 +- test/TestBase.py | 2 +- worlds/alttp/test/__init__.py | 3 +-- worlds/ror2/__init__.py | 1 - 7 files changed, 16 insertions(+), 11 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 10ad846432e1..ad50dcca85d1 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -200,7 +200,7 @@ def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tu self.player_types[new_id] = NetUtils.SlotType.group self._region_cache[new_id] = {} world_type = AutoWorld.AutoWorldRegister.world_types[game] - for option_key, option in world_type.option_definitions.items(): + for option_key, option in world_type.options_dataclass.type_hints.items(): getattr(self, option_key)[new_id] = option(option.default) for option_key, option in Options.common_options.items(): getattr(self, option_key)[new_id] = option(option.default) diff --git a/Generate.py b/Generate.py index 46784418ac1a..75f47e79b0c5 100644 --- a/Generate.py +++ b/Generate.py @@ -7,8 +7,8 @@ import string import urllib.parse import urllib.request -from collections import Counter, ChainMap -from typing import Dict, Tuple, Callable, Any, Union, get_type_hints +from collections import Counter +from typing import Dict, Tuple, Callable, Any, Union import ModuleUpdate @@ -339,7 +339,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 = get_type_hints(game_world.options_dataclass) + 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) @@ -464,7 +464,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default))) if ret.game in AutoWorldRegister.world_types: - for option_key, option in get_type_hints(world_type.options_dataclass).items(): + for option_key, option in world_type.options_dataclass.type_hints.items(): handle_option(ret, game_weights, option_key, option, plando_options) if PlandoOptions.items in plando_options: ret.plando_items = game_weights.get("plando_items", []) diff --git a/Options.py b/Options.py index 31956b766733..9a48986b35c0 100644 --- a/Options.py +++ b/Options.py @@ -861,8 +861,15 @@ class ProgressionBalancing(SpecialRange): } +class OptionsMetaProperty(type): + @property + def type_hints(cls) -> typing.Dict[str, AssembleOptions]: + """Returns type hints of the class as a dictionary.""" + return typing.get_type_hints(cls) + + @dataclass -class CommonOptions: +class CommonOptions(metaclass=OptionsMetaProperty): progression_balancing: ProgressionBalancing accessibility: Accessibility diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 117f57549b63..50e123151a4f 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -57,7 +57,7 @@ def get_html_doc(option_type: type(Options.Option)) -> str: for game_name, world in AutoWorldRegister.world_types.items(): - all_options: typing.Dict[str, Options.AssembleOptions] = typing.get_type_hints(world.options_dataclass) + all_options: typing.Dict[str, Options.AssembleOptions] = world.options_dataclass.type_hints with open(local_path("WebHostLib", "templates", "options.yaml")) as f: file_data = f.read() diff --git a/test/TestBase.py b/test/TestBase.py index 220ecc929909..983b20a1632f 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -120,7 +120,7 @@ def world_setup(self, seed: typing.Optional[int] = None) -> None: self.multiworld.set_seed(seed) self.multiworld.state = CollectionState(self.multiworld) args = Namespace() - for name, option in typing.get_type_hints(AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass).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"))) }) diff --git a/worlds/alttp/test/__init__.py b/worlds/alttp/test/__init__.py index 4efc3723ea5e..958b92b72567 100644 --- a/worlds/alttp/test/__init__.py +++ b/worlds/alttp/test/__init__.py @@ -1,6 +1,5 @@ import unittest from argparse import Namespace -from typing import get_type_hints from BaseClasses import MultiWorld, CollectionState from worlds import AutoWorldRegister @@ -11,6 +10,6 @@ def world_setup(self): self.multiworld = MultiWorld(1) self.multiworld.state = CollectionState(self.multiworld) args = Namespace() - for name, option in get_type_hints(AutoWorldRegister.world_types["A Link to the Past"].options_dataclass).items(): + 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) diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 4ee137968ea0..4b5c7bc00667 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -30,7 +30,6 @@ class RiskOfRainWorld(World): first crash landing. """ game = "Risk of Rain 2" - option_definitions = get_type_hints(ROR2Options) options_dataclass = ROR2Options o: ROR2Options topology_present = False From aadbd569e4f18f1ec2d893fd581e4fae77e96ba3 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Sat, 25 Feb 2023 13:29:05 +0100 Subject: [PATCH 34/88] move remaining usages from get_type_hints to metaclass property --- BaseClasses.py | 4 ++-- test/general/TestFill.py | 8 +++----- test/general/__init__.py | 8 ++++---- worlds/ror2/__init__.py | 1 - 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index ad50dcca85d1..5e94e1de3490 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -234,13 +234,13 @@ def set_options(self, args: Namespace) -> None: self.custom_data[player] = {} world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] self.worlds[player] = world_type(self, player) - for option_key in typing.get_type_hints(world_type.options_dataclass): + 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.GameOptions] = self.worlds[player].options_dataclass self.worlds[player].o = options_dataclass(**{option_key: getattr(args, option_key)[player] - for option_key in typing.get_type_hints(options_dataclass)}) + for option_key in options_dataclass.type_hints}) def set_item_links(self): item_links = {} diff --git a/test/general/TestFill.py b/test/general/TestFill.py index 5b9b1d9acaa8..d9ac6fcb9f75 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -1,6 +1,4 @@ -import itertools -from argparse import Namespace -from typing import get_type_hints, List, Iterable +from typing import List, Iterable import unittest import Options @@ -25,14 +23,14 @@ 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 get_type_hints(Options.PerGameCommonOptions).items(): + 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.o = world.options_dataclass(**{option_key: getattr(multi_world, option_key)[player_id] - for option_key in get_type_hints(world.options_dataclass)}) + for option_key in world.options_dataclass.type_hints}) multi_world.set_seed(0) diff --git a/test/general/__init__.py b/test/general/__init__.py index b6bede376fd7..c53bb23c215f 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -1,20 +1,20 @@ from argparse import Namespace -from typing import get_type_hints +from typing import Type from BaseClasses import MultiWorld, CollectionState -from worlds.AutoWorld import call_all +from worlds.AutoWorld import call_all, World gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"] -def setup_solo_multiworld(world_type) -> MultiWorld: +def setup_solo_multiworld(world_type: Type[World]) -> MultiWorld: multiworld = MultiWorld(1) multiworld.game[1] = world_type.game multiworld.player_name = {1: "Tester"} multiworld.set_seed() multiworld.state = CollectionState(multiworld) args = Namespace() - for name, option in get_type_hints(world_type.options_dataclass).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) for step in gen_steps: diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 4b5c7bc00667..5b4dccf6045d 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -1,5 +1,4 @@ import string -from typing import get_type_hints from .Items import RiskOfRainItem, item_table, item_pool_weights, environment_offest from .Locations import RiskOfRainLocation, get_classic_item_pickups, item_pickups, orderedstage_location From 2584535b004054bc31eb933014287c14998252b7 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Sat, 25 Feb 2023 14:52:10 +0100 Subject: [PATCH 35/88] move remaining usages from __annotations__ to metaclass property --- BaseClasses.py | 2 +- Options.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 5e94e1de3490..683417fee0f9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1586,7 +1586,7 @@ def write_option(option_key: str, option_obj: Union[type(Options.Option), "Optio outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player))) outfile.write('Game: %s\n' % self.multiworld.game[player]) - for f_option, option in self.multiworld.worlds[player].o.__annotations__.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) diff --git a/Options.py b/Options.py index 9a48986b35c0..3cd6841fdca9 100644 --- a/Options.py +++ b/Options.py @@ -873,17 +873,17 @@ class CommonOptions(metaclass=OptionsMetaProperty): progression_balancing: ProgressionBalancing accessibility: Accessibility - def as_dict(self, *args: str) -> typing.Dict[str, typing.Any]: + def as_dict(self, *option_names: str) -> typing.Dict[str, typing.Any]: """ Pass the option_names you would like returned as a dictionary as strings. Returns a dictionary of [str, Option.value] """ option_results = {} - for option_name in args: - if option_name in self.__annotations__: + for option_name in option_names: + if option_name in type(self).type_hints: option_results[option_name] = getattr(self, option_name).value else: - raise ValueError(f"{option_name} not found in {self.__annotations__}") + raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}") return option_results From 0ae2acc4ca4778f3ebab2d6d4f64537bab4c9cc6 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Sat, 25 Feb 2023 13:35:34 +0100 Subject: [PATCH 36/88] move remaining usages from legacy dictionaries to metaclass property --- BaseClasses.py | 6 ------ Generate.py | 9 +++++---- Utils.py | 1 - 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 683417fee0f9..0ca5ca8b32d2 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,8 +1,6 @@ from __future__ import annotations -import collections import copy -import itertools import functools import json import logging @@ -202,10 +200,6 @@ def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tu world_type = AutoWorld.AutoWorldRegister.world_types[game] for option_key, option in world_type.options_dataclass.type_hints.items(): getattr(self, option_key)[new_id] = option(option.default) - for option_key, option in Options.common_options.items(): - getattr(self, option_key)[new_id] = option(option.default) - for option_key, option in Options.per_game_common_options.items(): - getattr(self, option_key)[new_id] = option(option.default) self.worlds[new_id] = world_type(self, new_id) self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id]) diff --git a/Generate.py b/Generate.py index 75f47e79b0c5..a90fe2419c80 100644 --- a/Generate.py +++ b/Generate.py @@ -156,7 +156,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}.") @@ -444,8 +445,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) @@ -460,7 +461,7 @@ 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))) if ret.game in AutoWorldRegister.world_types: diff --git a/Utils.py b/Utils.py index 777c3283935c..133f1c452e06 100644 --- a/Utils.py +++ b/Utils.py @@ -1,7 +1,6 @@ from __future__ import annotations import asyncio -import types import typing import builtins import os From a147aaee869984b5ad2f4a73064cc90fa2e86539 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Sat, 25 Feb 2023 13:45:53 +0100 Subject: [PATCH 37/88] remove legacy dictionaries --- Options.py | 8 -------- worlds/AutoWorld.py | 3 +-- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Options.py b/Options.py index 3cd6841fdca9..13c1a7a4b1e7 100644 --- a/Options.py +++ b/Options.py @@ -887,10 +887,6 @@ def as_dict(self, *option_names: str) -> typing.Dict[str, typing.Any]: return option_results -common_options = typing.get_type_hints(CommonOptions) -# TODO - remove this dict once all worlds use options dataclasses - - class ItemSet(OptionSet): verify_item_name = True convert_name_groups = True @@ -1016,10 +1012,6 @@ class PerGameCommonOptions(CommonOptions): item_links: ItemLinks -per_game_common_options = typing.get_type_hints(PerGameCommonOptions) -# TODO - remove this dict once all worlds use options dataclasses - - GameOptions = typing.TypeVar("GameOptions", bound=PerGameCommonOptions) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 2a1792014aba..10143edde0a3 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -7,7 +7,7 @@ from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Type, Union, TYPE_CHECKING, \ ClassVar -from Options import AssembleOptions, GameOptions, PerGameCommonOptions +from Options import GameOptions, PerGameCommonOptions from BaseClasses import CollectionState if TYPE_CHECKING: @@ -138,7 +138,6 @@ 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]] = {} # TODO - remove this once all worlds use options dataclasses options_dataclass: Type[GameOptions] = PerGameCommonOptions # link your Options mapping o: PerGameCommonOptions From 88db4f70f5bc5803934e2b4214b7e5490e0d3f12 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Sat, 25 Feb 2023 14:24:45 +0100 Subject: [PATCH 38/88] cache the metaclass property --- Options.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Options.py b/Options.py index 13c1a7a4b1e7..5c460b16a2e9 100644 --- a/Options.py +++ b/Options.py @@ -2,6 +2,7 @@ import abc from copy import deepcopy from dataclasses import dataclass +import functools import math import numbers import typing @@ -863,6 +864,7 @@ class ProgressionBalancing(SpecialRange): class OptionsMetaProperty(type): @property + @functools.lru_cache(maxsize=None) def type_hints(cls) -> typing.Dict[str, AssembleOptions]: """Returns type hints of the class as a dictionary.""" return typing.get_type_hints(cls) From 01374e0dab303ded5b8bf057d93299fb19de0329 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Sat, 25 Feb 2023 13:20:02 +0100 Subject: [PATCH 39/88] clarify inheritance in world api --- docs/world api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/world api.md b/docs/world api.md index c7e3691b6531..11fb167df9a2 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -329,6 +329,7 @@ class FixXYZGlitch(Toggle): display_name = "Fix XYZ Glitch" # By convention, we call the options dataclass `Options`. +# It has to be derived from 'PerGameCommonOptions'. @dataclass class MyGameOptions(PerGameCommonOptions): difficulty: Difficulty From ac123dbb9d1ce838d5afd0b6b89a77c6c7c39540 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 13 Mar 2023 18:07:41 -0500 Subject: [PATCH 40/88] move the messenger to new options system --- worlds/messenger/Options.py | 26 ++++++++++++++------------ worlds/messenger/Rules.py | 15 +++++++++------ worlds/messenger/SubClasses.py | 9 ++++++--- worlds/messenger/__init__.py | 25 +++++++++++++------------ 4 files changed, 42 insertions(+), 33 deletions(-) diff --git a/worlds/messenger/Options.py b/worlds/messenger/Options.py index 1baca12e3ab9..600f6c202fdf 100644 --- a/worlds/messenger/Options.py +++ b/worlds/messenger/Options.py @@ -1,4 +1,6 @@ -from Options import DefaultOnToggle, DeathLink, Range, Accessibility, Choice +from dataclasses import dataclass + +from Options import DefaultOnToggle, DeathLink, Range, Accessibility, Choice, PerGameCommonOptions class MessengerAccessibility(Accessibility): @@ -53,14 +55,14 @@ class RequiredSeals(Range): default = range_end -messenger_options = { - "accessibility": MessengerAccessibility, - "enable_logic": Logic, - "shuffle_seals": PowerSeals, - "goal": Goal, - "music_box": MusicBox, - "notes_needed": NotesNeeded, - "total_seals": AmountSeals, - "percent_seals_required": RequiredSeals, - "death_link": DeathLink, -} +@dataclass +class MessengerOptions(PerGameCommonOptions): + accessibility: MessengerAccessibility + enable_logic: Logic + shuffle_seals: PowerSeals + goal: Goal + music_box: MusicBox + notes_needed: NotesNeeded + total_seals: AmountSeals + percent_seals_required: RequiredSeals + death_link: DeathLink diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py index c2731678025a..ee7710942e00 100644 --- a/worlds/messenger/Rules.py +++ b/worlds/messenger/Rules.py @@ -88,11 +88,12 @@ def has_vertical(self, state: CollectionState) -> bool: return self.has_wingsuit(state) or self.has_dart(state) def has_enough_seals(self, state: CollectionState) -> bool: - required_seals = state.multiworld.worlds[self.player].required_seals + required_seals = self.world.required_seals return state.has("Power Seal", self.player, required_seals) def set_messenger_rules(self) -> None: multiworld = self.world.multiworld + options = self.world.o for region in multiworld.get_regions(self.player): if region.name in self.region_rules: @@ -101,16 +102,16 @@ def set_messenger_rules(self) -> None: for loc in region.locations: if loc.name in self.location_rules: loc.access_rule = self.location_rules[loc.name] - if multiworld.goal[self.player] == Goal.option_power_seal_hunt: + if 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)) - if multiworld.enable_logic[self.player]: + if options.enable_logic: multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player) else: multiworld.accessibility[self.player].value = MessengerAccessibility.option_minimal if multiworld.accessibility[self.player] > MessengerAccessibility.option_locations: - set_self_locking_items(multiworld, self.player) + set_self_locking_items(self.world, self.player) def location_item_name(state: CollectionState, location_name: str, player: int) -> Optional[Tuple[str, int]]: @@ -146,13 +147,15 @@ def add_allowed_rules(area: Union[Location, Entrance], location: Location) -> No add_allowed_rules(spot, spot) -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("Key of Strength", player), "Power Thistle") allow_self_locking_items(multiworld.get_location("Key of Love", player), "Sun Crest", "Moon Crest") allow_self_locking_items(multiworld.get_location("Key of Courage", player), "Demon King Crown") # add these locations when seals aren't shuffled - if not multiworld.shuffle_seals[player]: + if not world.o.shuffle_seals: allow_self_locking_items(multiworld.get_region("Cloud Ruins", player), "Ruxxtin's Amulet") allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS) diff --git a/worlds/messenger/SubClasses.py b/worlds/messenger/SubClasses.py index 32803f5e0d1b..d994e076d9f1 100644 --- a/worlds/messenger/SubClasses.py +++ b/worlds/messenger/SubClasses.py @@ -12,18 +12,21 @@ class MessengerRegion(Region): + world: MessengerWorld + def __init__(self, name: str, world: MessengerWorld): super().__init__(name, world.player, world.multiworld) - self.add_locations(self.multiworld.worlds[self.player].location_name_to_id) + self.world = world + self.add_locations(world.location_name_to_id) world.multiworld.regions.append(self) def add_locations(self, name_to_id: Dict[str, int]) -> None: for loc in REGIONS[self.name]: self.locations.append(MessengerLocation(loc, self, name_to_id.get(loc, None))) - if self.name == "The Shop" and self.multiworld.goal[self.player] > Goal.option_open_music_box: + if self.name == "The Shop" and self.world.o.goal > Goal.option_open_music_box: self.locations.append(MessengerLocation("Shop Chest", self, name_to_id.get("Shop Chest", None))) # putting some dumb special case for searing crags and ToT so i can split them into 2 regions - if self.multiworld.shuffle_seals[self.player] and self.name not in {"Searing Crags", "Tower HQ"}: + if self.world.o.shuffle_seals and self.name not in {"Searing Crags", "Tower HQ"}: for seal_loc in SEALS: if seal_loc.startswith(self.name.split(" ")[0]): self.locations.append(MessengerLocation(seal_loc, self, name_to_id.get(seal_loc, None))) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 1c42b30494a9..ccb00a445974 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -3,7 +3,7 @@ from BaseClasses import Tutorial, ItemClassification from worlds.AutoWorld import World, WebWorld from .Constants import NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS, ALWAYS_LOCATIONS, SEALS, ALL_ITEMS -from .Options import messenger_options, NotesNeeded, Goal, PowerSeals +from .Options import MessengerOptions, NotesNeeded, Goal, PowerSeals from .Regions import REGIONS, REGION_CONNECTIONS from .Rules import MessengerRules from .SubClasses import MessengerRegion, MessengerItem @@ -43,7 +43,8 @@ class MessengerWorld(World): "Shuriken": {"Windmill Shuriken"}, } - option_definitions = messenger_options + options_dataclass = MessengerOptions + o: MessengerOptions base_offset = 0xADD_000 item_name_to_id = {item: item_id @@ -59,10 +60,10 @@ class MessengerWorld(World): required_seals: Optional[int] = None 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 - self.required_seals = int(self.multiworld.percent_seals_required[self.player].value / 100 * self.total_seals) + if self.o.goal == Goal.option_power_seal_hunt: + self.o.shuffle_seals.value = PowerSeals.option_true + self.total_seals = self.o.total_seals.value + self.required_seals = int(self.o.percent_seals_required.value / 100 * self.total_seals) def create_regions(self) -> None: for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]: @@ -71,14 +72,14 @@ def create_regions(self) -> None: def create_items(self) -> None: itempool: List[MessengerItem] = [] - if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt: + if self.o.goal == Goal.option_power_seal_hunt: seals = [self.create_item("Power Seal") for _ in range(self.total_seals)] for i in range(self.required_seals): seals[i].classification = ItemClassification.progression_skip_balancing itempool += seals else: notes = self.multiworld.random.sample(NOTES, k=len(NOTES)) - precollected_notes_amount = NotesNeeded.range_end - self.multiworld.notes_needed[self.player] + precollected_notes_amount = NotesNeeded.range_end - self.o.notes_needed if precollected_notes_amount: for note in notes[:precollected_notes_amount]: self.multiworld.push_precollected(self.create_item(note)) @@ -109,12 +110,12 @@ def fill_slot_data(self) -> Dict[str, Any]: locations[loc.address] = [loc.item.name, self.multiworld.player_name[loc.item.player]] 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.o.death_link.value, + "goal": self.o.goal.current_key, + "music_box": self.o.music_box.value, "required_seals": self.required_seals, "locations": locations, - "settings": {"Difficulty": "Basic" if not self.multiworld.shuffle_seals[self.player] else "Advanced"} + "settings": {"Difficulty": "Basic" if not self.o.shuffle_seals else "Advanced"} } def get_filler_item_name(self) -> str: From e6806ed8f53973b28ce7030122163519ecf8e8e5 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 13 Mar 2023 18:40:11 -0500 Subject: [PATCH 41/88] add an assert for my dumb --- Options.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Options.py b/Options.py index babaace6fcfb..b4ec54d19df7 100644 --- a/Options.py +++ b/Options.py @@ -878,6 +878,12 @@ class ProgressionBalancing(SpecialRange): class OptionsMetaProperty(type): + def __new__(mcs, name, bases, attrs): + for attr, attr_type in attrs.items(): + 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, AssembleOptions]: From 2c8afbc5f45088adb8eea545125298d265e02198 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 14 Mar 2023 14:19:57 -0500 Subject: [PATCH 42/88] update the doc --- docs/options api.md | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/options api.md b/docs/options api.md index a1407f2cebc0..effd8578d57b 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -27,14 +27,16 @@ options: ```python # Options.py +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 -} +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 @@ -42,27 +44,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.o.my_option_name`. This is 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 @@ -114,7 +119,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() ``` @@ -122,7 +127,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() ``` @@ -154,7 +159,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 From f8ba777c0003a7a6826dd86507ac3898535346ae Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 14 Mar 2023 14:26:19 -0500 Subject: [PATCH 43/88] rename o to options --- BaseClasses.py | 8 +-- Fill.py | 10 ++-- Main.py | 22 ++++----- docs/world api.md | 21 +++++--- test/general/TestFill.py | 6 +-- worlds/AutoWorld.py | 2 +- worlds/factorio/__init__.py | 6 +-- worlds/messenger/Rules.py | 2 +- worlds/messenger/SubClasses.py | 4 +- worlds/messenger/__init__.py | 22 ++++----- worlds/ror2/__init__.py | 90 +++++++++++++++++----------------- 11 files changed, 99 insertions(+), 94 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index a0a72af637bc..020bd0dfd694 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -237,14 +237,14 @@ def set_options(self, args: Namespace) -> None: setattr(self, option_key, option_values) # TODO - remove this loop once all worlds use options dataclasses options_dataclass: typing.Type[Options.GameOptions] = self.worlds[player].options_dataclass - self.worlds[player].o = options_dataclass(**{option_key: getattr(args, option_key)[player] - for option_key in options_dataclass.type_hints}) + 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.worlds[player].o.item_links.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']}") @@ -1250,7 +1250,7 @@ def get_path(state, region): def to_file(self, filename: str): def write_option(option_key: str, option_obj: type(Options.Option)): - res = getattr(self.multiworld.worlds[player].o, option_key) + res = getattr(self.multiworld.worlds[player].options, option_key) display_name = getattr(option_obj, "display_name", option_key) try: outfile.write(f'{display_name + ":":33}{res.current_option_name}\n') diff --git a/Fill.py b/Fill.py index f03a14d72390..2c5b71b653f6 100644 --- a/Fill.py +++ b/Fill.py @@ -56,7 +56,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.worlds[item_to_place.player].o.accessibility == Accessibility.option_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 @@ -221,7 +221,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.worlds[player].o.accessibility == "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: @@ -244,7 +244,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.worlds[item.player].o.accessibility != '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) @@ -487,9 +487,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.worlds[player].o.progression_balancing / 100 + player: world.worlds[player].options.progression_balancing / 100 for player in world.player_ids - if world.worlds[player].o.progression_balancing > 0 + if world.worlds[player].options.progression_balancing > 0 } if not balanceable_players: logging.info('Skipping multiworld progression balancing.') diff --git a/Main.py b/Main.py index dd2905db6e7b..742e4ac53ccf 100644 --- a/Main.py +++ b/Main.py @@ -112,7 +112,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.worlds[player].o.start_inventory.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,21 +130,21 @@ 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.worlds[player].o.non_local_items.value -= world.worlds[player].o.local_items.value - world.worlds[player].o.non_local_items.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]) if world.players > 1: locality_rules(world) else: - world.worlds[1].o.non_local_items.value = set() - world.worlds[1].o.local_items.value = set() + world.worlds[1].options.non_local_items.value = set() + world.worlds[1].options.local_items.value = set() AutoWorld.call_all(world, "set_rules") for player in world.player_ids: - exclusion_rules(world, player, world.worlds[player].o.exclude_locations.value) - world.worlds[player].o.priority_locations.value -= world.worlds[player].o.exclude_locations.value - for location_name in world.worlds[player].o.priority_locations.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: world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY AutoWorld.call_all(world, "generate_basic") @@ -347,11 +347,11 @@ def precollect_hint(location): f" {location}" locations_data[location.player][location.address] = \ location.item.code, location.item.player, location.item.flags - if location.name in world.worlds[location.player].o.start_location_hints: + if location.name in world.worlds[location.player].options.start_location_hints: precollect_hint(location) - elif location.item.name in world.worlds[location.item.player].o.start_hints: + elif location.item.name in world.worlds[location.item.player].options.start_hints: precollect_hint(location) - elif any([location.item.name in world.worlds[player].o.start_hints + 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) diff --git a/docs/world api.md b/docs/world api.md index 6a5a5c9a764a..ff769ad93500 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -336,17 +336,19 @@ class MyGameOptions(PerGameCommonOptions): final_boss_hp: FinalBossHP fix_xyz_glitch: FixXYZGlitch ``` + ```python # __init__.py from worlds.AutoWorld import World from .Options import MyGameOptions # import the options dataclass + class MyGameWorld(World): - #... + # ... options_dataclass = MyGameOptions # assign the options dataclass to the world - o: MyGameOptions # typing for option results - #... + options: MyGameOptions # typing for option results + # ... ``` ### A World Class Skeleton @@ -361,17 +363,20 @@ from worlds.AutoWorld import World from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification from Utils import get_options, output_path + class MyGameItem(Item): # or from Items import MyGameItem game = "My Game" # name of the game/world this item is from + class MyGameLocation(Location): # or from Locations import MyGameLocation game = "My Game" # name of the game/world this location is in + class MyGameWorld(World): """Insert description of the world/game here.""" game: str = "My Game" # name of the game/world options_dataclass = MyGameOptions # options the player can set - o: MyGameOptions # typing for option results + options: MyGameOptions # typing for option results topology_present: bool = True # show path to required location checks in spoiler # data_version is used to signal that items, locations or their names @@ -451,7 +456,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.o.final_boss_hp.value + self.final_boss_hp = self.options.final_boss_hp.value ``` #### create_item @@ -666,7 +671,7 @@ def generate_output(self, output_directory: str): "seed": self.multiworld.seed_name, # to verify the server's multiworld "slot": self.multiworld.player_name[self.player], # to connect to server "items": {location.name: location.item.name - if location.item.player == self.player else "Remote" + if location.item.player == self.player else "Remote" for location in self.multiworld.get_filled_locations(self.player)}, # store start_inventory from player's .yaml # make sure to mark as not remote_start_inventory when connecting if stored in rom/mod @@ -674,9 +679,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.o.difficulty.current_key, + "difficulty": self.options.difficulty.current_key, # store option value True or False for fixing a glitch - "fix_xyz_glitch": self.o.fix_xyz_glitch.value + "fix_xyz_glitch": self.options.fix_xyz_glitch.value } # point to a ROM specified by the installation src = Utils.get_options()["mygame_options"]["rom_file"] diff --git a/test/general/TestFill.py b/test/general/TestFill.py index d9ac6fcb9f75..6055fd382ee9 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -29,8 +29,8 @@ def generate_multi_world(players: int = 1) -> MultiWorld: 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.o = world.options_dataclass(**{option_key: getattr(multi_world, option_key)[player_id] - for option_key in world.options_dataclass.type_hints}) + 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) @@ -197,7 +197,7 @@ def test_minimal_fill(self): items = player1.prog_items locations = player1.locations - multi_world.worlds[player1.id].o.accessibility = Accessibility.from_any(Accessibility.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( diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index e9c670329601..36e9df1920ce 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -140,7 +140,7 @@ class World(metaclass=AutoWorldRegister): options_dataclass: Type[GameOptions] = PerGameCommonOptions """link your Options mapping""" - o: PerGameCommonOptions + options: PerGameCommonOptions """resulting options for the player of this world""" game: ClassVar[str] diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index a25e084819b8..ab738d6b89c9 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -73,9 +73,9 @@ def __init__(self, world, player: int): generate_output = generate_mod def generate_early(self) -> None: - self.o.max_tech_cost = max(self.o.max_tech_cost, self.o.min_tech_cost) - self.tech_mix = self.o.tech_cost_mix - self.skip_silo = self.o.silo.value == Silo.option_spawn + self.options.max_tech_cost = max(self.options.max_tech_cost, self.options.min_tech_cost) + self.tech_mix = self.options.tech_cost_mix + self.skip_silo = self.options.silo.value == Silo.option_spawn def create_regions(self): player = self.player diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py index 625e352c6d41..d15a7d7100fe 100644 --- a/worlds/messenger/Rules.py +++ b/worlds/messenger/Rules.py @@ -93,7 +93,7 @@ def has_enough_seals(self, state: CollectionState) -> bool: def set_messenger_rules(self) -> None: multiworld = self.world.multiworld - options = self.world.o + options = self.world.options for region in multiworld.get_regions(self.player): if region.name in self.region_rules: diff --git a/worlds/messenger/SubClasses.py b/worlds/messenger/SubClasses.py index d994e076d9f1..936ee4054cec 100644 --- a/worlds/messenger/SubClasses.py +++ b/worlds/messenger/SubClasses.py @@ -23,10 +23,10 @@ def __init__(self, name: str, world: MessengerWorld): def add_locations(self, name_to_id: Dict[str, int]) -> None: for loc in REGIONS[self.name]: self.locations.append(MessengerLocation(loc, self, name_to_id.get(loc, None))) - if self.name == "The Shop" and self.world.o.goal > Goal.option_open_music_box: + if self.name == "The Shop" and self.world.options.goal > Goal.option_open_music_box: self.locations.append(MessengerLocation("Shop Chest", self, name_to_id.get("Shop Chest", None))) # putting some dumb special case for searing crags and ToT so i can split them into 2 regions - if self.world.o.shuffle_seals and self.name not in {"Searing Crags", "Tower HQ"}: + if self.world.options.shuffle_seals and self.name not in {"Searing Crags", "Tower HQ"}: for seal_loc in SEALS: if seal_loc.startswith(self.name.split(" ")[0]): self.locations.append(MessengerLocation(seal_loc, self, name_to_id.get(seal_loc, None))) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index ccb00a445974..76245bfebd20 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -44,7 +44,7 @@ class MessengerWorld(World): } options_dataclass = MessengerOptions - o: MessengerOptions + options: MessengerOptions base_offset = 0xADD_000 item_name_to_id = {item: item_id @@ -60,10 +60,10 @@ class MessengerWorld(World): required_seals: Optional[int] = None def generate_early(self) -> None: - if self.o.goal == Goal.option_power_seal_hunt: - self.o.shuffle_seals.value = PowerSeals.option_true - self.total_seals = self.o.total_seals.value - self.required_seals = int(self.o.percent_seals_required.value / 100 * self.total_seals) + 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.required_seals = int(self.options.percent_seals_required.value / 100 * self.total_seals) def create_regions(self) -> None: for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]: @@ -72,14 +72,14 @@ def create_regions(self) -> None: def create_items(self) -> None: itempool: List[MessengerItem] = [] - if self.o.goal == Goal.option_power_seal_hunt: + if self.options.goal == Goal.option_power_seal_hunt: seals = [self.create_item("Power Seal") for _ in range(self.total_seals)] for i in range(self.required_seals): seals[i].classification = ItemClassification.progression_skip_balancing itempool += seals else: notes = self.multiworld.random.sample(NOTES, k=len(NOTES)) - precollected_notes_amount = NotesNeeded.range_end - self.o.notes_needed + precollected_notes_amount = NotesNeeded.range_end - self.options.notes_needed if precollected_notes_amount: for note in notes[:precollected_notes_amount]: self.multiworld.push_precollected(self.create_item(note)) @@ -110,12 +110,12 @@ def fill_slot_data(self) -> Dict[str, Any]: locations[loc.address] = [loc.item.name, self.multiworld.player_name[loc.item.player]] return { - "deathlink": self.o.death_link.value, - "goal": self.o.goal.current_key, - "music_box": self.o.music_box.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, "locations": locations, - "settings": {"Difficulty": "Basic" if not self.o.shuffle_seals else "Advanced"} + "settings": {"Difficulty": "Basic" if not self.options.shuffle_seals else "Advanced"} } def get_filler_item_name(self) -> str: diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index e91b897d1e9c..544fb91dad2e 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -30,7 +30,7 @@ class RiskOfRainWorld(World): """ game = "Risk of Rain 2" options_dataclass = ROR2Options - o: ROR2Options + options: ROR2Options topology_present = False item_name_to_id = item_table @@ -43,44 +43,44 @@ class RiskOfRainWorld(World): def generate_early(self) -> None: # figure out how many revivals should exist in the pool - if self.o.goal == "classic": - total_locations = self.o.total_locations.value + if self.options.goal == "classic": + total_locations = self.options.total_locations.value else: total_locations = len( orderedstage_location.get_locations( - chests=self.o.chests_per_stage.value, - shrines=self.o.shrines_per_stage.value, - scavengers=self.o.scavengers_per_stage.value, - scanners=self.o.scanner_per_stage.value, - altars=self.o.altars_per_stage.value, - dlc_sotv=self.o.dlc_sotv.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.o.total_revivals.value / 100 * + self.total_revivals = int(self.options.total_revivals.value / 100 * total_locations) - if self.o.start_with_revive: + 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.o.start_with_revive: + 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.o.goal == "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.o.dlc_sotv: + 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.o.dlc_sotv: + 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.o.begin_with_loop 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) @@ -88,9 +88,9 @@ def create_items(self) -> None: environments_pool.pop(unlock[0]) # if presets are enabled generate junk_pool from the selected preset - pool_option = self.o.item_weights.value + pool_option = self.options.item_weights.value junk_pool: Dict[str, int] = {} - if self.o.item_pool_presets: + 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(): @@ -99,24 +99,24 @@ def create_items(self) -> None: junk_pool = item_pool_weights[pool_option].copy() else: # generate junk pool from user created presets junk_pool = { - "Item Scrap, Green": self.o.green_scrap.value, - "Item Scrap, Red": self.o.red_scrap.value, - "Item Scrap, Yellow": self.o.yellow_scrap.value, - "Item Scrap, White": self.o.white_scrap.value, - "Common Item": self.o.common_item.value, - "Uncommon Item": self.o.uncommon_item.value, - "Legendary Item": self.o.legendary_item.value, - "Boss Item": self.o.boss_item.value, - "Lunar Item": self.o.lunar_item.value, - "Void Item": self.o.void_item.value, - "Equipment": self.o.equipment.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.o.enable_lunar 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.o.dlc_sotv or pool_option == ItemWeights.option_void: + if not self.options.dlc_sotv or pool_option == ItemWeights.option_void: junk_pool.pop("Void Item") # Generate item pool @@ -129,19 +129,19 @@ def create_items(self) -> None: # precollected environments are popped from the pool so counting like this is valid nonjunk_item_count = self.total_revivals + len(environments_pool) - if self.o.goal == "classic": + if self.options.goal == "classic": # classic mode - total_locations = self.o.total_locations.value + total_locations = self.options.total_locations.value else: # explore mode total_locations = len( orderedstage_location.get_locations( - chests=self.o.chests_per_stage.value, - shrines=self.o.shrines_per_stage.value, - scavengers=self.o.scavengers_per_stage.value, - scanners=self.o.scanner_per_stage.value, - altars=self.o.altars_per_stage.value, - dlc_sotv=self.o.dlc_sotv.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 ) ) junk_item_count = total_locations - nonjunk_item_count @@ -158,7 +158,7 @@ def set_rules(self) -> None: def create_regions(self) -> None: - if self.o.goal == "classic": + if self.options.goal == "classic": # classic mode menu = create_region(self.multiworld, self.player, "Menu") self.multiworld.regions.append(menu) @@ -167,7 +167,7 @@ def create_regions(self) -> None: 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.o.total_locations.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 @@ -185,7 +185,7 @@ def create_regions(self) -> None: create_events(self.multiworld, self.player) def fill_slot_data(self): - options_dict = self.o.as_dict("item_pickup_step", "shrine_use_step", "goal", "total_locations", + 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") @@ -225,12 +225,12 @@ def create_item(self, name: str) -> Item: 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) @@ -238,7 +238,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) From 984d6594376b235e4794e0571ebed7738bf59803 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 14 Mar 2023 14:43:12 -0500 Subject: [PATCH 44/88] missed a spot --- worlds/messenger/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py index d15a7d7100fe..a47036bfd79a 100644 --- a/worlds/messenger/Rules.py +++ b/worlds/messenger/Rules.py @@ -123,6 +123,6 @@ def set_self_locking_items(world: MessengerWorld, player: int) -> None: allow_self_locking_items(multiworld.get_location("Key of Courage", player), "Demon King Crown") # add these locations when seals aren't shuffled - if not world.o.shuffle_seals: + if not world.options.shuffle_seals: allow_self_locking_items(multiworld.get_region("Cloud Ruins", player), "Ruxxtin's Amulet") allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS) From e959c49600dffbe2c213b0cbfa951b0c0de9d32b Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 1 Apr 2023 13:08:29 -0500 Subject: [PATCH 45/88] update new messenger options --- worlds/messenger/Rules.py | 7 +++---- worlds/messenger/__init__.py | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py index 8b0e8d4a3756..af31b5791e9a 100644 --- a/worlds/messenger/Rules.py +++ b/worlds/messenger/Rules.py @@ -98,7 +98,6 @@ def true(self, state: CollectionState) -> bool: def set_messenger_rules(self) -> None: multiworld = self.world.multiworld - options = self.world.options for region in multiworld.get_regions(self.player): if region.name in self.region_rules: @@ -107,7 +106,7 @@ def set_messenger_rules(self) -> None: for loc in region.locations: if loc.name in self.location_rules: loc.access_rule = self.location_rules[loc.name] - if options.goal == 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)) @@ -154,7 +153,7 @@ def has_windmill(self, state: CollectionState) -> bool: 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 add_rule(self.world.multiworld.get_location(loc, self.player), rule, "or") @@ -210,7 +209,7 @@ def __init__(self, world: MessengerWorld) -> None: 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(world: MessengerWorld, player: int) -> None: diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index e9987dfb3eb9..e7bafbf7842e 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -101,7 +101,7 @@ def create_items(self) -> None: 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: Rules.MessengerRules(self).set_messenger_rules() elif logic == Logic.option_hard: @@ -133,5 +133,5 @@ def get_filler_item_name(self) -> str: def create_item(self, name: str) -> MessengerItem: item_id: Optional[int] = self.item_name_to_id.get(name, None) override_prog = name in {"Windmill Shuriken"} and getattr(self, "multiworld") is not None \ - and self.multiworld.logic_level[self.player] > Logic.option_normal + and self.options.logic_level > Logic.option_normal return MessengerItem(name, self.player, item_id, override_prog) From ec8c75b41a8dea117b5e7e33ca8ec46a130e5aaf Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 1 Apr 2023 18:23:22 -0500 Subject: [PATCH 46/88] comment spacing Co-authored-by: Doug Hoskisson --- docs/world api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/world api.md b/docs/world api.md index 47734f9dfb72..b1b1ff7cdd9a 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -375,7 +375,7 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation class MyGameWorld(World): """Insert description of the world/game here.""" game = "My Game" # name of the game/world - options_dataclass = MyGameOptions # options the player can set + options_dataclass = MyGameOptions # options the player can set options: MyGameOptions # typing hints for option results topology_present = True # show path to required location checks in spoiler From 2b46533170e54a8d88cc330e48579af8fe914ce2 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 20 May 2023 20:26:35 -0500 Subject: [PATCH 47/88] fix tests --- Options.py | 5 +---- test/general/TestHelpers.py | 2 +- test/general/TestOptions.py | 2 +- worlds/stardew_valley/test/__init__.py | 6 +++--- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Options.py b/Options.py index 6a1cc374ea36..e0a880afa7e6 100644 --- a/Options.py +++ b/Options.py @@ -1094,10 +1094,7 @@ def dictify_range(option: typing.Union[Range, SpecialRange]): 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() diff --git a/test/general/TestHelpers.py b/test/general/TestHelpers.py index b6b1ea470178..543162c0865c 100644 --- a/test/general/TestHelpers.py +++ b/test/general/TestHelpers.py @@ -1,3 +1,4 @@ +from argparse import Namespace from typing import Dict, Optional, Callable from BaseClasses import MultiWorld, CollectionState, Region @@ -13,7 +14,6 @@ def setUp(self) -> None: 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] = { diff --git a/test/general/TestOptions.py b/test/general/TestOptions.py index b7058183e09c..4a3bd0b02a0a 100644 --- a/test/general/TestOptions.py +++ b/test/general/TestOptions.py @@ -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__) diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 9d2fac02d937..99e6d4e0f09a 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -1,7 +1,7 @@ from argparse import Namespace from typing import Dict, FrozenSet, Tuple, Any, ClassVar -from BaseClasses import MultiWorld +from BaseClasses import MultiWorld, CollectionState from test.TestBase import WorldTestBase from test.general import gen_steps from .. import StardewValleyWorld @@ -42,12 +42,12 @@ def setup_solo_multiworld(test_options=None, multiworld.game[1] = StardewValleyWorld.game multiworld.player_name = {1: "Tester"} multiworld.set_seed() + multiworld.state = CollectionState(multiworld) 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) - multiworld.set_default_common_options() for step in gen_steps: call_all(multiworld, step) From e6424e29d3ea9b8faa14f4445c974dd6b83a2537 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 21 May 2023 01:12:19 -0500 Subject: [PATCH 48/88] fix missing import --- worlds/messenger/Options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/messenger/Options.py b/worlds/messenger/Options.py index 9397ec57cfcc..6a1d1f578e10 100644 --- a/worlds/messenger/Options.py +++ b/worlds/messenger/Options.py @@ -1,6 +1,7 @@ from dataclasses import dataclass -from Options import DefaultOnToggle, DeathLink, Range, Accessibility, Choice, Toggle, StartInventoryPool +from Options import DefaultOnToggle, DeathLink, Range, Accessibility, Choice, Toggle, StartInventoryPool,\ + PerGameCommonOptions class MessengerAccessibility(Accessibility): From f189f5d274e52555c988d8ae717212127ec1a9e1 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 25 May 2023 11:08:46 -0500 Subject: [PATCH 49/88] make the documentation definition more accurate --- docs/options api.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/options api.md b/docs/options api.md index 720e393205ba..3f7a9b56a182 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -28,11 +28,12 @@ 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 @@ -41,6 +42,7 @@ class StartingSword(Toggle): display_name = "Start With Sword" +@dataclass class ExampleGameOptions(PerGameCommonOptions): starting_sword: StartingSword ``` From 3e1be1e6757bc9f90743d211c21271ed9e9d3e97 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 27 Jun 2023 22:51:34 -0500 Subject: [PATCH 50/88] use options system for loc creation --- worlds/messenger/SubClasses.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/worlds/messenger/SubClasses.py b/worlds/messenger/SubClasses.py index c83f8f5e0892..4d3c1e8d0f3b 100644 --- a/worlds/messenger/SubClasses.py +++ b/worlds/messenger/SubClasses.py @@ -2,7 +2,7 @@ from BaseClasses import Region, Location, Item, ItemClassification, Entrance, CollectionState from .Constants import NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS -from .Options import Goal +from .Options import Goal, MessengerOptions from .Regions import REGIONS, SEALS, MEGA_SHARDS from .Shop import SHOP_ITEMS, PROG_SHOP_ITEMS, USEFUL_SHOP_ITEMS, FIGURINES @@ -13,17 +13,22 @@ class MessengerRegion(Region): + def __init__(self, name: str, world: MessengerWorld) -> None: super().__init__(name, world.player, world.multiworld) - self.add_locations(self.multiworld.worlds[self.player].location_name_to_id) + self.add_locations(world.location_name_to_id) + if name == "The Shop" and world.options.goal > Goal.option_open_music_box: + self.locations.append(MessengerLocation("Shop Chest", self, None)) + if world.options.shuffle_seals and name in SEALS: + self.create_seal_locs(world.location_name_to_id) + if world.options.shuffle_shards and name in MEGA_SHARDS: + self.create_shard_locs(world.location_name_to_id) world.multiworld.regions.append(self) def add_locations(self, name_to_id: Dict[str, int]) -> None: for loc in REGIONS[self.name]: self.locations.append(MessengerLocation(loc, self, name_to_id.get(loc, None))) if self.name == "The Shop": - if self.multiworld.goal[self.player] > Goal.option_open_music_box: - self.locations.append(MessengerLocation("Shop Chest", self, None)) self.locations += [MessengerShopLocation(f"The Shop - {shop_loc}", self, name_to_id[f"The Shop - {shop_loc}"]) for shop_loc in SHOP_ITEMS] @@ -31,12 +36,12 @@ def add_locations(self, name_to_id: Dict[str, int]) -> None: for figurine in FIGURINES] elif self.name == "Tower HQ": self.locations.append(MessengerLocation("Money Wrench", self, name_to_id["Money Wrench"])) - if self.multiworld.shuffle_seals[self.player] and self.name in SEALS: - self.locations += [MessengerLocation(seal_loc, self, name_to_id[seal_loc]) - for seal_loc in SEALS[self.name]] - if self.multiworld.shuffle_shards[self.player] and self.name in MEGA_SHARDS: - self.locations += [MessengerLocation(shard, self, name_to_id[shard]) - for shard in MEGA_SHARDS[self.name]] + + def create_seal_locs(self, name_to_id: Dict[str, int]) -> None: + self.locations += [MessengerLocation(seal_loc, self, name_to_id[seal_loc]) for seal_loc in SEALS[self.name]] + + def create_shard_locs(self, name_to_id: Dict[str, int]) -> None: + self.locations += [MessengerLocation(shard_loc, self, name_to_id[shard_loc]) for shard_loc in MEGA_SHARDS[self.name]] def add_exits(self, exits: Set[str]) -> None: for exit in exits: From fc93f971ce5dbeca9c6416d6b2e86be2cb30d03b Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 27 Jun 2023 23:08:28 -0500 Subject: [PATCH 51/88] type cast MessengerWorld --- worlds/messenger/SubClasses.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/worlds/messenger/SubClasses.py b/worlds/messenger/SubClasses.py index 4d3c1e8d0f3b..4f336ac255e9 100644 --- a/worlds/messenger/SubClasses.py +++ b/worlds/messenger/SubClasses.py @@ -1,15 +1,13 @@ -from typing import Set, TYPE_CHECKING, Optional, Dict +from typing import Set, TYPE_CHECKING, Optional, Dict, cast from BaseClasses import Region, Location, Item, ItemClassification, Entrance, CollectionState from .Constants import NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS -from .Options import Goal, MessengerOptions +from .Options import Goal from .Regions import REGIONS, SEALS, MEGA_SHARDS from .Shop import SHOP_ITEMS, PROG_SHOP_ITEMS, USEFUL_SHOP_ITEMS, FIGURINES if TYPE_CHECKING: from . import MessengerWorld -else: - MessengerWorld = object class MessengerRegion(Region): @@ -62,11 +60,11 @@ def __init__(self, name: str, parent: MessengerRegion, loc_id: Optional[int]) -> class MessengerShopLocation(MessengerLocation): 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: MessengerWorld = cast(MessengerWorld, self.parent_region.multiworld.worlds[self.player]) return world.shop_prices.get(name, world.figurine_prices.get(name)) def can_afford(self, state: CollectionState) -> bool: - world: MessengerWorld = state.multiworld.worlds[self.player] + world: MessengerWorld = cast(MessengerWorld, state.multiworld.worlds[self.player]) cost = self.cost() * 2 if cost >= 1000: cost *= 2 From 0b9959e7dbe17198e5b106c26f12266d8b66ca1d Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 27 Jun 2023 23:28:19 -0500 Subject: [PATCH 52/88] fix typo and use quotes for cast --- worlds/messenger/SubClasses.py | 6 +++--- worlds/messenger/__init__.py | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/worlds/messenger/SubClasses.py b/worlds/messenger/SubClasses.py index 4f336ac255e9..925e2d4440f3 100644 --- a/worlds/messenger/SubClasses.py +++ b/worlds/messenger/SubClasses.py @@ -12,7 +12,7 @@ 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) self.add_locations(world.location_name_to_id) if name == "The Shop" and world.options.goal > Goal.option_open_music_box: @@ -60,11 +60,11 @@ def __init__(self, name: str, parent: MessengerRegion, loc_id: Optional[int]) -> class MessengerShopLocation(MessengerLocation): def cost(self) -> int: name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped - world: MessengerWorld = cast(MessengerWorld, self.parent_region.multiworld.worlds[self.player]) + world: MessengerWorld = cast("MessengerWorld", self.parent_region.multiworld.worlds[self.player]) return world.shop_prices.get(name, world.figurine_prices.get(name)) def can_afford(self, state: CollectionState) -> bool: - world: MessengerWorld = cast(MessengerWorld, state.multiworld.worlds[self.player]) + world: MessengerWorld = cast("MessengerWorld", state.multiworld.worlds[self.player]) cost = self.cost() * 2 if cost >= 1000: cost *= 2 diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index b45c5fd60f1b..36d13b686c80 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -107,9 +107,7 @@ def create_items(self) -> None: # 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.multiworld.per_slot_randoms[self.player].shuffle(notes) - precollected_notes_amount = NotesNeeded.range_end - \ - self.options.notes_needed[self.player] - \ - (len(NOTES) - len(notes)) + precollected_notes_amount = NotesNeeded.range_end - self.options.notes_needed - (len(NOTES) - len(notes)) if precollected_notes_amount: for note in notes[:precollected_notes_amount]: self.multiworld.push_precollected(self.create_item(note)) From 266acd2ee42cd3fa98b100da2a7ffb3d99326eb9 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 18 Jul 2023 22:39:44 -0500 Subject: [PATCH 53/88] LTTP: set random seed in tests --- worlds/alttp/test/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/alttp/test/__init__.py b/worlds/alttp/test/__init__.py index 958b92b72567..5baaa7e88e61 100644 --- a/worlds/alttp/test/__init__.py +++ b/worlds/alttp/test/__init__.py @@ -9,6 +9,7 @@ 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"))}) From 4596be95591b6c81fff379e4bbe3aab0ca604256 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 23 Jul 2023 16:37:51 -0500 Subject: [PATCH 54/88] ArchipIdle: remove change here as it's default on AutoWorld --- worlds/archipidle/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/worlds/archipidle/__init__.py b/worlds/archipidle/__init__.py index 86636b6c3098..2d182f31dc20 100644 --- a/worlds/archipidle/__init__.py +++ b/worlds/archipidle/__init__.py @@ -1,5 +1,4 @@ from BaseClasses import Item, MultiWorld, Region, Location, Entrance, Tutorial, ItemClassification -from Options import PerGameCommonOptions from .Items import item_table from .Rules import set_rules from ..AutoWorld import World, WebWorld @@ -38,8 +37,6 @@ class ArchipIDLEWorld(World): hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April web = ArchipIDLEWebWorld() - options_dataclass = PerGameCommonOptions - item_name_to_id = {} start_id = 9000 for item in item_table: From 41ed5451c58aa517c18dfd36c010ff49a9e0b788 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 23 Jul 2023 16:38:13 -0500 Subject: [PATCH 55/88] Stardew: Need to set state because `set_default_common_options` used to --- worlds/stardew_valley/test/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index b17048f29d58..5bd3014f943c 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -109,6 +109,7 @@ def setup_solo_multiworld(test_options=None, seed=None, multiworld = MultiWorld(1) multiworld.game[1] = StardewValleyWorld.game multiworld.player_name = {1: "Tester"} + multiworld.state = CollectionState(multiworld) multiworld.set_seed(seed) # print(f"Seed: {multiworld.seed}") # Uncomment to print the seed for every test args = Namespace() From 26976b8f427c85678735c923237ab759f44c3e84 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 23 Jul 2023 16:39:06 -0500 Subject: [PATCH 56/88] The Messenger: update shop rando and helpers to new system; optimize imports --- worlds/messenger/Constants.py | 4 ++-- worlds/messenger/Options.py | 7 ++++--- worlds/messenger/Regions.py | 2 +- worlds/messenger/Rules.py | 10 +++++----- worlds/messenger/Shop.py | 5 ++--- worlds/messenger/SubClasses.py | 10 +++++----- worlds/messenger/__init__.py | 2 +- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/worlds/messenger/Constants.py b/worlds/messenger/Constants.py index 121584da0555..e0e471fd4cd5 100644 --- a/worlds/messenger/Constants.py +++ b/worlds/messenger/Constants.py @@ -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", diff --git a/worlds/messenger/Options.py b/worlds/messenger/Options.py index 91f09b5ff01e..1da544bee70c 100644 --- a/worlds/messenger/Options.py +++ b/worlds/messenger/Options.py @@ -1,9 +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,\ - PerGameCommonOptions +from schema import And, Optional, Or, Schema + +from Options import Accessibility, Choice, DeathLink, DefaultOnToggle, OptionDict, PerGameCommonOptions, Range, \ + StartInventoryPool, Toggle class MessengerAccessibility(Accessibility): diff --git a/worlds/messenger/Regions.py b/worlds/messenger/Regions.py index 2bfd3cab8433..0178bea95841 100644 --- a/worlds/messenger/Regions.py +++ b/worlds/messenger/Regions.py @@ -1,4 +1,4 @@ -from typing import Dict, Set, List +from typing import Dict, List, Set REGIONS: Dict[str, List[str]] = { "Menu": [], diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py index 2799e5ffb326..23f76217c78c 100644 --- a/worlds/messenger/Rules.py +++ b/worlds/messenger/Rules.py @@ -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: @@ -215,7 +215,7 @@ def set_messenger_rules(self) -> None: for loc, rule in self.extra_rules.items(): 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") diff --git a/worlds/messenger/Shop.py b/worlds/messenger/Shop.py index 68f415349b7b..be82c09058c2 100644 --- a/worlds/messenger/Shop.py +++ b/worlds/messenger/Shop.py @@ -1,4 +1,3 @@ -from random import Random from typing import Dict, TYPE_CHECKING, NamedTuple, Tuple, List if TYPE_CHECKING: @@ -73,8 +72,8 @@ class ShopData(NamedTuple): 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] = {} diff --git a/worlds/messenger/SubClasses.py b/worlds/messenger/SubClasses.py index e1e717108a60..36ce892ec563 100644 --- a/worlds/messenger/SubClasses.py +++ b/worlds/messenger/SubClasses.py @@ -1,10 +1,10 @@ -from typing import TYPE_CHECKING, Optional, cast +from typing import Optional, TYPE_CHECKING, cast -from BaseClasses import Region, Location, Item, ItemClassification, CollectionState -from .Constants import NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS +from BaseClasses import CollectionState, Item, ItemClassification, Location, Region +from .Constants import NOTES, PHOBEKINS, PROG_ITEMS, USEFUL_ITEMS from .Options import Goal -from .Regions import REGIONS, SEALS, MEGA_SHARDS -from .Shop import SHOP_ITEMS, PROG_SHOP_ITEMS, USEFUL_SHOP_ITEMS, FIGURINES +from .Regions import MEGA_SHARDS, REGIONS, SEALS +from .Shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS if TYPE_CHECKING: from . import MessengerWorld diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index f85ae6084361..afb0dbe6a89f 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -118,7 +118,7 @@ def create_items(self) -> None: 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.options.percent_seals_required.value / 100 * self.total_seals) From 7a114d472aa00095cb4dd20c37f6cd105c0aadf6 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 23 Jul 2023 17:02:43 -0500 Subject: [PATCH 57/88] Add a kwarg to `as_dict` to do the casing for you --- Options.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Options.py b/Options.py index 501a9a68af90..9e063f2a5dd5 100644 --- a/Options.py +++ b/Options.py @@ -912,15 +912,30 @@ class CommonOptions(metaclass=OptionsMetaProperty): progression_balancing: ProgressionBalancing accessibility: Accessibility - def as_dict(self, *option_names: str) -> typing.Dict[str, typing.Any]: + def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]: """ - Pass the option_names you would like returned as a dictionary as strings. 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: - option_results[option_name] = getattr(self, option_name).value + 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 From bb06b3a50bca0a99569fb53f3348d2961e449336 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 23 Jul 2023 17:02:58 -0500 Subject: [PATCH 58/88] RoR2: use new kwarg for less code --- worlds/ror2/__init__.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index a5294f91fd91..4955827d504d 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -188,16 +188,9 @@ 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") - cased_dict = {} - for key, value in options_dict.items(): - split_name = [name.title() for name in key.split("_")] - split_name[0] = split_name[0].lower() - new_name = "".join(split_name) - cased_dict[new_name] = value - + "final_stage_death", "death_link", casing="camel") return { - **cased_dict, + **options_dict, "seed": "".join(self.multiworld.per_slot_randoms[self.player].choice(string.digits) for _ in range(16)), } From f5da39e7031a3b2c0f6d439e2536a8852c40b348 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 1 Sep 2023 19:30:51 -0500 Subject: [PATCH 59/88] RoR2: revert some accidental reverts --- worlds/ror2/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 23ea7bb00b47..22c65dd9deb7 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -100,19 +100,19 @@ def create_items(self) -> None: 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 From 9cf454ba05ce86b3a7caf3f9b1348cdbaca60e52 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 1 Sep 2023 19:31:08 -0500 Subject: [PATCH 60/88] The Messenger: remove an unnecessary variable --- worlds/messenger/SubClasses.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/messenger/SubClasses.py b/worlds/messenger/SubClasses.py index 3fe016770cdf..bec751432366 100644 --- a/worlds/messenger/SubClasses.py +++ b/worlds/messenger/SubClasses.py @@ -70,8 +70,7 @@ def cost(self) -> int: def can_afford(self, state: CollectionState) -> bool: world = cast("MessengerWorld", state.multiworld.worlds[self.player]) - cost = self.cost - can_afford = state.has("Shards", self.player, min(cost, world.total_shards)) + 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) From 37f6d8b448c0e8e45a69132feee91accafcfdcab Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Fri, 1 Sep 2023 21:07:19 -0700 Subject: [PATCH 61/88] remove TypeVar that isn't used --- BaseClasses.py | 2 +- Options.py | 15 +++++++-------- worlds/AutoWorld.py | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index f01c6fc2cd89..877ec931ebb2 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -237,7 +237,7 @@ def set_options(self, args: Namespace) -> None: 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.GameOptions] = self.worlds[player].options_dataclass + 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}) diff --git a/Options.py b/Options.py index 9adea3261f0d..8801c75a4a3b 100644 --- a/Options.py +++ b/Options.py @@ -899,16 +899,19 @@ class ProgressionBalancing(SpecialRange): } -class OptionsMetaProperty(type): - def __new__(mcs, name, bases, attrs): - for attr, attr_type in attrs.items(): +class OptionsMetaProperty(abc.ABCMeta): + 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, AssembleOptions]: + 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) @@ -1077,10 +1080,6 @@ class PerGameCommonOptions(CommonOptions): item_links: ItemLinks -GameOptions = typing.TypeVar("GameOptions", bound=PerGameCommonOptions) - - - def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True): import os diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index d750cbefa896..9bdd5af31d97 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -8,7 +8,7 @@ from typing import Any, Callable, ClassVar, Dict, Set, Tuple, FrozenSet, List, Optional, TYPE_CHECKING, TextIO, Type, \ Union -from Options import GameOptions, PerGameCommonOptions +from Options import PerGameCommonOptions from BaseClasses import CollectionState if TYPE_CHECKING: @@ -170,7 +170,7 @@ 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.""" - options_dataclass: Type[GameOptions] = PerGameCommonOptions + options_dataclass: ClassVar[Type[PerGameCommonOptions]] = PerGameCommonOptions """link your Options mapping""" options: PerGameCommonOptions """resulting options for the player of this world""" From d381d1dfccf76468a0c66c557ccd7ca977e124fb Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sat, 2 Sep 2023 07:46:46 -0700 Subject: [PATCH 62/88] CommonOptions not abstract --- Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Options.py b/Options.py index 8801c75a4a3b..8ce3d90a9638 100644 --- a/Options.py +++ b/Options.py @@ -899,7 +899,7 @@ class ProgressionBalancing(SpecialRange): } -class OptionsMetaProperty(abc.ABCMeta): +class OptionsMetaProperty(type): def __new__(mcs, name: str, bases: typing.Tuple[type, ...], From 6d418f90a70782107877c0920922546ae346d1e5 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 2 Sep 2023 17:06:08 -0500 Subject: [PATCH 63/88] Docs: fix mistake in options api.md Co-authored-by: Doug Hoskisson --- docs/options api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/options api.md b/docs/options api.md index 3f7a9b56a182..2c86833800c7 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -65,7 +65,7 @@ class ExampleWorld(World): ### 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.o.my_option_name`. 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 From 2e30f07533977d154027c88458e1c423c300dd42 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 2 Sep 2023 23:25:00 -0500 Subject: [PATCH 64/88] create options for item link worlds --- BaseClasses.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BaseClasses.py b/BaseClasses.py index 877ec931ebb2..d37237b8c329 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -202,11 +202,15 @@ def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tu self.player_types[new_id] = NetUtils.SlotType.group self._region_cache[new_id] = {} world_type = AutoWorld.AutoWorldRegister.world_types[game] + # TODO - remove this loop once all worlds use options dataclasses for option_key, option in world_type.options_dataclass.type_hints.items(): getattr(self, option_key)[new_id] = option(option.default) self.worlds[new_id] = world_type(self, new_id) self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id]) + self.worlds[new_id].options = world_type.options_dataclass(**{ + option_key: option(option.default) for option_key, option in world_type.options_dataclass.type_hints.items() + }) self.player_name[new_id] = name new_group = self.groups[new_id] = Group(name=name, game=game, players=players, From 8583c2a03c515e8dc8303848a158a9fe7fc36b36 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 2 Sep 2023 23:25:33 -0500 Subject: [PATCH 65/88] revert accidental doc removals --- docs/world api.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/world api.md b/docs/world api.md index 8b395a00812b..26f97bc31275 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -395,6 +395,7 @@ class MyGameWorld(World): game = "My Game" # name of the game/world 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 # ID of first item and location, could be hard-coded but code may be easier @@ -682,7 +683,7 @@ def generate_output(self, output_directory: str): "seed": self.multiworld.seed_name, # to verify the server's multiworld "slot": self.multiworld.player_name[self.player], # to connect to server "items": {location.name: location.item.name - if location.item.player == self.player else "Remote" + if location.item.player == self.player else "Remote" for location in self.multiworld.get_filled_locations(self.player)}, # store start_inventory from player's .yaml # make sure to mark as not remote_start_inventory when connecting if stored in rom/mod From 833f09c2d37b28a3c7e4ddb63f74682aec7924b1 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Wed, 27 Sep 2023 17:37:30 -0500 Subject: [PATCH 66/88] Item Links: set default options on group --- worlds/AutoWorld.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 76d183c32f8e..9a8b6a56ef36 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -372,16 +372,14 @@ def get_filler_item_name(self) -> str: 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 - - 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(): + # 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()}) - 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]: From 06ba9ba76d1c54b023f91f3e96887d4645af7693 Mon Sep 17 00:00:00 2001 From: beauxq Date: Wed, 27 Sep 2023 18:41:09 -0700 Subject: [PATCH 67/88] change Zillion to new options dataclass --- worlds/zillion/__init__.py | 14 +++- worlds/zillion/options.py | 127 +++++++++++++---------------- worlds/zillion/test/TestOptions.py | 10 ++- 3 files changed, 75 insertions(+), 76 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 7c927c10eb92..40475656bd46 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -13,7 +13,7 @@ from .config import detect_test 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 @@ def generate_early(self) -> None: 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, self.player) + + if zz_op.early_scope: + self.multiworld.early_items[self.player]["Scope"] = 1 self._item_counts = item_counts @@ -299,7 +304,8 @@ def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: 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: diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 6aa88f5b220f..7ff23ebd796d 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -1,13 +1,13 @@ 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 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 +41,12 @@ class VBLR(Choice): option_restrictive = 3 default = 1 + def to_zz_vblr(self) -> ZzVBLR: + key = self.current_key + assert key in ("vanilla", "balanced", "low", "restrictive"), f"{key=}" + return key # type: ignore + # mypy can't do type narrowing to literals + class ZillionGunLevels(VBLR): """ @@ -225,27 +231,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 +268,35 @@ def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts: return tr -def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": +def validate(options: ZillionOptions, p: int) -> "Tuple[ZzOptions, Counter[str]]": """ adjusts options to make game completion possible - `world` parameter is MultiWorld object that has my options on it + `options` parameter is ZillionOptions object that was put on my world by the core `p` is my player id """ - 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 +329,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 +341,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]) - room_gen = cast(ZillionRoomGen, wo.room_gen[p]) + starting_cards = options.starting_cards - 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) diff --git a/worlds/zillion/test/TestOptions.py b/worlds/zillion/test/TestOptions.py index 1ec186dae50a..bb91ae438d11 100644 --- a/worlds/zillion/test/TestOptions.py +++ b/worlds/zillion/test/TestOptions.py @@ -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, 1) 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 @@ def test_vblr_ap_to_zz(self) -> None: 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, 1) assert getattr(zz_options, option_name) in VBLR_CHOICES # TODO: test validate with invalid combinations of options From 7d09402b157e8c17aab29f2843f2d9a20395965d Mon Sep 17 00:00:00 2001 From: beauxq Date: Wed, 27 Sep 2023 18:48:38 -0700 Subject: [PATCH 68/88] remove unused parameter to function --- worlds/zillion/__init__.py | 2 +- worlds/zillion/options.py | 3 +-- worlds/zillion/test/TestOptions.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 40475656bd46..c39511728d4a 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -144,7 +144,7 @@ def generate_early(self) -> None: if not hasattr(self.multiworld, "zillion_logic_cache"): setattr(self.multiworld, "zillion_logic_cache", {}) - zz_op, item_counts = validate(self.options, self.player) + zz_op, item_counts = validate(self.options) if zz_op.early_scope: self.multiworld.early_items[self.player]["Scope"] = 1 diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 7ff23ebd796d..e03ed9bb7899 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -268,12 +268,11 @@ def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts: return tr -def validate(options: ZillionOptions, p: int) -> "Tuple[ZzOptions, Counter[str]]": +def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": """ adjusts options to make game completion possible `options` parameter is ZillionOptions object that was put on my world by the core - `p` is my player id """ skill = options.skill.value diff --git a/worlds/zillion/test/TestOptions.py b/worlds/zillion/test/TestOptions.py index bb91ae438d11..c4f02d4bd3be 100644 --- a/worlds/zillion/test/TestOptions.py +++ b/worlds/zillion/test/TestOptions.py @@ -11,7 +11,7 @@ def test_validate_default(self) -> None: self.world_setup() options = self.multiworld.worlds[1].options assert isinstance(options, ZillionOptions) - validate(options, 1) + 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 """ @@ -24,7 +24,7 @@ def test_vblr_ap_to_zz(self) -> None: self.world_setup() options = self.multiworld.worlds[1].options assert isinstance(options, ZillionOptions) - zz_options, _item_counts = validate(options, 1) + zz_options, _item_counts = validate(options) assert getattr(zz_options, option_name) in VBLR_CHOICES # TODO: test validate with invalid combinations of options From ddb630bcd659a295558acfcb730b29b6b53c089d Mon Sep 17 00:00:00 2001 From: beauxq Date: Sun, 1 Oct 2023 10:01:11 -0700 Subject: [PATCH 69/88] use TypeGuard for Literal narrowing --- worlds/zillion/options.py | 14 +++++++++++--- worlds/zillion/requirements.txt | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index e03ed9bb7899..80f9469ec8c0 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -1,6 +1,7 @@ from collections import Counter 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 @@ -42,10 +43,17 @@ class VBLR(Choice): 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 key in ("vanilla", "balanced", "low", "restrictive"), f"{key=}" - return key # type: ignore - # mypy can't do type narrowing to literals + assert is_vblr(key), f"{key=}" + return key class ZillionGunLevels(VBLR): diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt index 2af057decef9..69ae1d04cb4c 100644 --- a/worlds/zillion/requirements.txt +++ b/worlds/zillion/requirements.txt @@ -1 +1,2 @@ zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@4b27d115269db25fe73b0471b73495f41df1323c#0.5.3 +typing-extensions>=4.7, <5 From 668d5986687296648784a98d3ed95ae3f623bd02 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 7 Oct 2023 12:23:14 -0500 Subject: [PATCH 70/88] move dlc quest to new api --- Options.py | 6 ++ worlds/dlcquest/Options.py | 56 ++++------------- worlds/dlcquest/Regions.py | 44 ++++++------- worlds/dlcquest/Rules.py | 122 ++++++++++++++++++------------------ worlds/dlcquest/__init__.py | 27 ++++---- 5 files changed, 112 insertions(+), 143 deletions(-) diff --git a/Options.py b/Options.py index 8ce3d90a9638..d9ddfc2e2fdb 100644 --- a/Options.py +++ b/Options.py @@ -214,6 +214,12 @@ def __gt__(self, other: typing.Union[int, NumericOption]) -> bool: 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) diff --git a/worlds/dlcquest/Options.py b/worlds/dlcquest/Options.py index a1674a4d5a8b..ce728b4e9244 100644 --- a/worlds/dlcquest/Options.py +++ b/worlds/dlcquest/Options.py @@ -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 diff --git a/worlds/dlcquest/Regions.py b/worlds/dlcquest/Regions.py index 8135a1c362c5..5dc094b8fcfb 100644 --- a/worlds/dlcquest/Regions.py +++ b/worlds/dlcquest/Regions.py @@ -26,16 +26,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 +43,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 +58,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 +68,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 +191,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 +220,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 +260,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 +269,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 diff --git a/worlds/dlcquest/Rules.py b/worlds/dlcquest/Rules.py index d2495121f4e8..f9e99fd893ea 100644 --- a/worlds/dlcquest/Rules.py +++ b/worlds/dlcquest/Rules.py @@ -42,7 +42,7 @@ def has_coin(state, player: int, coins: int): 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) diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index 9569d0efcc1a..f3e10f26d6aa 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -35,10 +35,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 +66,8 @@ def create_items(self): 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 +78,11 @@ def create_item(self, item: Union[str, ItemData]) -> DLCQuestItem: 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 From b952aaaa0c1607821531b689bcc035b6b7125477 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 7 Oct 2023 12:32:59 -0500 Subject: [PATCH 71/88] move overcooked 2 to new api --- worlds/overcooked2/Options.py | 49 ++++++++++++++--------------- worlds/overcooked2/__init__.py | 57 +++++++++++++++------------------- 2 files changed, 49 insertions(+), 57 deletions(-) diff --git a/worlds/overcooked2/Options.py b/worlds/overcooked2/Options.py index 9ddcf5e85ff5..f58ed3aa4c23 100644 --- a/worlds/overcooked2/Options.py +++ b/worlds/overcooked2/Options.py @@ -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_preserver_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 diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index afffa744fa8a..80ebeb519a0c 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -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 @@ def add_level_location( 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() @@ -231,26 +226,25 @@ def get_priority_locations(self) -> List[int]: 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"]: 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, ) @@ -369,11 +363,11 @@ def create_items(self): # 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 @@ def create_items(self): # 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: @@ -416,7 +410,7 @@ def create_items(self): 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 @@ def fill_json_data(self) -> Dict[str, Any]: # 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)] = { @@ -506,7 +500,7 @@ def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]: 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 @@ def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]: "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 @@ def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]: # 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_preserver_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] From 8a5c15762d66599961774889bd9d98146ef2a448 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 7 Oct 2023 13:02:26 -0500 Subject: [PATCH 72/88] fixed some missed code in oc2 --- worlds/overcooked2/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index 80ebeb519a0c..688e23f75d3a 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -6,7 +6,7 @@ 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 @@ -155,7 +155,7 @@ def get_n_random_locations(self, n: int) -> List[int]: 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) @@ -238,7 +238,7 @@ def generate_early(self): 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, @@ -271,7 +271,7 @@ def create_regions(self) -> None: # 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") @@ -330,7 +330,7 @@ def create_regions(self) -> None: 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 @@ -398,7 +398,7 @@ def create_items(self): # 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: @@ -475,7 +475,7 @@ def fill_json_data(self) -> Dict[str, Any]: 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: From bcec62efcc10915c61dab9682384589748f48420 Mon Sep 17 00:00:00 2001 From: Alex Gilbert Date: Sat, 7 Oct 2023 15:55:39 -0400 Subject: [PATCH 73/88] - Tried to be compliant with 993 (WIP?) --- Options.py | 18 +- worlds/stardew_valley/__init__.py | 59 +- worlds/stardew_valley/bundles.py | 2 +- worlds/stardew_valley/items.py | 188 +++--- worlds/stardew_valley/locations.py | 160 ++--- worlds/stardew_valley/logic.py | 107 ++-- worlds/stardew_valley/mods/logic/deepwoods.py | 4 +- worlds/stardew_valley/mods/logic/magic.py | 16 +- worlds/stardew_valley/mods/logic/skills.py | 14 +- .../mods/logic/skullcavernelevator.py | 4 +- worlds/stardew_valley/options.py | 84 +-- worlds/stardew_valley/regions.py | 33 +- worlds/stardew_valley/rules.py | 595 +++++++++--------- .../test/TestLogicSimplification.py | 3 +- worlds/stardew_valley/test/TestOptions.py | 37 +- worlds/stardew_valley/test/TestRegions.py | 12 +- .../stardew_valley/test/long/option_names.py | 7 +- 17 files changed, 677 insertions(+), 666 deletions(-) diff --git a/Options.py b/Options.py index d9ddfc2e2fdb..cf1e1d3c4311 100644 --- a/Options.py +++ b/Options.py @@ -3,7 +3,7 @@ import abc import logging from copy import deepcopy -from dataclasses import dataclass +from dataclasses import dataclass, field import functools import math import numbers @@ -1076,14 +1076,14 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P @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 + local_items: LocalItems = LocalItems.default + non_local_items: NonLocalItems = NonLocalItems.default + start_inventory: StartInventory = field(default_factory=StartInventory) + start_hints: StartHints = StartHints.default + start_location_hints: StartLocationHints = StartLocationHints.default + exclude_locations: ExcludeLocations = ExcludeLocations.default + priority_locations: PriorityLocations = PriorityLocations.default + item_links: ItemLinks = field(default_factory=ItemLinks) def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True): diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 150e3589b073..1d367654d666 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -3,12 +3,12 @@ from BaseClasses import Region, Entrance, Location, Item, Tutorial, CollectionState, ItemClassification, MultiWorld 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 from .regions import create_regions from .rules import set_rules from worlds.generic.Rules import set_rule @@ -50,7 +50,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 +58,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 +72,24 @@ def __init__(self, world: MultiWorld, player: int): 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 == options.Goal.option_greatest_walnut_hunter + goal_is_perfection = self.options.goal == options.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 == options.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 = options.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 +115,7 @@ def create_items(self): 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 == options.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 +133,12 @@ def create_items(self): self.setup_victory() def precollect_starting_season(self) -> Optional[StardewItem]: - if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive: + if self.options.season_randomization == options.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 == options.SeasonRandomization.option_disabled: for season in season_pool: self.multiworld.push_precollected(self.create_item(season)) return @@ -148,18 +147,18 @@ def precollect_starting_season(self) -> Optional[StardewItem]: 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 == options.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] == + if (self.options.building_progression == options.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 == options.BackpackProgression.option_early_progressive: self.multiworld.early_items[self.player]["Progressive Backpack"] = 1 def setup_month_events(self): @@ -172,39 +171,39 @@ def setup_month_events(self): 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: + if self.options.goal == options.Goal.option_community_center: self.create_event_location(location_table[Goal.community_center], self.logic.can_complete_community_center().simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_grandpa_evaluation: + elif self.options.goal == options.Goal.option_grandpa_evaluation: self.create_event_location(location_table[Goal.grandpa_evaluation], self.logic.can_finish_grandpa_evaluation().simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_bottom_of_the_mines: + elif self.options.goal == options.Goal.option_bottom_of_the_mines: self.create_event_location(location_table[Goal.bottom_of_the_mines], self.logic.can_mine_to_floor(120).simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_cryptic_note: + elif self.options.goal == options.Goal.option_cryptic_note: self.create_event_location(location_table[Goal.cryptic_note], self.logic.can_complete_quest("Cryptic Note").simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_master_angler: + elif self.options.goal == options.Goal.option_master_angler: self.create_event_location(location_table[Goal.master_angler], self.logic.can_catch_every_fish().simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_complete_collection: + elif self.options.goal == options.Goal.option_complete_collection: self.create_event_location(location_table[Goal.complete_museum], self.logic.can_complete_museum().simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_full_house: + elif self.options.goal == options.Goal.option_full_house: self.create_event_location(location_table[Goal.full_house], (self.logic.has_children(2) & self.logic.can_reproduce()).simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_greatest_walnut_hunter: + elif self.options.goal == options.Goal.option_greatest_walnut_hunter: self.create_event_location(location_table[Goal.greatest_walnut_hunter], self.logic.has_walnut(130).simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_perfection: + elif self.options.goal == options.Goal.option_perfection: self.create_event_location(location_table[Goal.perfection], self.logic.has_everything(self.all_progression_items).simplify(), "Victory") @@ -230,7 +229,7 @@ def create_event_location(self, location_data: LocationData, rule: StardewRule, 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): @@ -278,7 +277,7 @@ def fill_slot_data(self) -> Dict[str, Any]: excluded_options = [options.BundleRandomization, options.BundlePrice, options.NumberOfMovementBuffs, options.NumberOfLuckBuffs] - slot_data = dict(self.options.options) + slot_data = dict(self.options.as_dict()) for option in excluded_options: slot_data.pop(option.internal_name) slot_data.update({ diff --git a/worlds/stardew_valley/bundles.py b/worlds/stardew_valley/bundles.py index 7cbb13923705..4af21542a4ec 100644 --- a/worlds/stardew_valley/bundles.py +++ b/worlds/stardew_valley/bundles.py @@ -152,7 +152,7 @@ def parse_stardew_bundle_items(string_objects: str) -> List[BundleItem]: # 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] diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index 1035997034da..d5c71dae4694 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -7,10 +7,11 @@ 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 @@ def initialize_item_table(): 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 diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py index 67bffa139632..345796b0311e 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -4,10 +4,12 @@ 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 diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py index 377fa0d03ba3..b2841d1566da 100644 --- a/worlds/stardew_valley/logic.py +++ b/worlds/stardew_valley/logic.py @@ -4,7 +4,6 @@ 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.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 @@ 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 @@ def __post_init__(self): 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 @@ def __post_init__(self): 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 @@ def __post_init__(self): 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 @@ def can_have_earned_total_money(self, amount: int) -> StardewRule: 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 @@ def has_tool(self, tool: str, material: str = ToolMaterial.basic) -> StardewRule 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 @@ def has_skill_level(self, skill: str, level: int) -> StardewRule: 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 @@ def has_total_skill_level(self, level: int, allow_modded_skills: bool = False) - 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 @@ def has_total_skill_level(self, level: int, allow_modded_skills: bool = False) - 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 @@ def has_house(self, upgrade_level: int) -> StardewRule: 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 @@ def can_get_combat_xp(self) -> StardewRule: 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 @@ def can_fish(self, difficulty: int = 0) -> StardewRule: 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 @@ def can_fish_chests(self) -> StardewRule: 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 @@ def can_buy_sapling(self, fruit: str) -> StardewRule: 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 @@ def can_catch_fish(self, fish: FishItem) -> StardewRule: 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 @@ def can_do_panning(self, item: str = Generic.any) -> StardewRule: 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 @@ def can_chop_perfectly(self) -> StardewRule: 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 @@ def can_progress_in_the_mines_from_floor(self, floor: int) -> StardewRule: 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 @@ def can_progress_easily_in_the_mines_from_floor(self, floor: int) -> StardewRule 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 @@ def can_progress_in_the_skull_cavern_from_floor(self, floor: int) -> StardewRule 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 @@ def can_mine_to_skull_cavern_floor(self, floor: int) -> StardewRule: 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 @@ def can_reproduce(self, number_children: int = 1) -> StardewRule: 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 @@ def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule: 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 @@ def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule: 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 @@ def can_earn_relationship(self, npc: str, hearts: int = 0) -> StardewRule: 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 @@ def has_year_three(self) -> StardewRule: 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 @@ def can_find_museum_items(self, number: int) -> StardewRule: 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 @@ def has_season(self, season: str) -> StardewRule: 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 @@ def has_lived_months(self, number: int) -> StardewRule: 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 @@ def can_succeed_luau_soup(self) -> StardewRule: 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 @@ def can_open_geode(self, geode: str) -> StardewRule: 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 @@ def can_forage(self, season: str, region: str = Region.forest, need_hoe: bool = 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 @@ def can_water(self, level: int) -> StardewRule: 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") diff --git a/worlds/stardew_valley/mods/logic/deepwoods.py b/worlds/stardew_valley/mods/logic/deepwoods.py index f6ecd6d82806..2aa90e5b76b6 100644 --- a/worlds/stardew_valley/mods/logic/deepwoods.py +++ b/worlds/stardew_valley/mods/logic/deepwoods.py @@ -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)) diff --git a/worlds/stardew_valley/mods/logic/magic.py b/worlds/stardew_valley/mods/logic/magic.py index a084c6aa9199..709376399c87 100644 --- a/worlds/stardew_valley/mods/logic/magic.py +++ b/worlds/stardew_valley/mods/logic/magic.py @@ -7,19 +7,19 @@ 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) diff --git a/worlds/stardew_valley/mods/logic/skills.py b/worlds/stardew_valley/mods/logic/skills.py index 05b4c623e157..24402a088b16 100644 --- a/worlds/stardew_valley/mods/logic/skills.py +++ b/worlds/stardew_valley/mods/logic/skills.py @@ -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) diff --git a/worlds/stardew_valley/mods/logic/skullcavernelevator.py b/worlds/stardew_valley/mods/logic/skullcavernelevator.py index 74db86d89aac..9a5140ae39c9 100644 --- a/worlds/stardew_valley/mods/logic/skullcavernelevator.py +++ b/worlds/stardew_valley/mods/logic/skullcavernelevator.py @@ -4,7 +4,7 @@ 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_() diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index 78de9e8dbbaa..e1486dd0a6c9 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -1,29 +1,8 @@ from dataclasses import dataclass -from typing import Dict, Union, Protocol, runtime_checkable, ClassVar -from Options import Option, Range, DeathLink, SpecialRange, Toggle, Choice, OptionSet +from Options import Range, SpecialRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink 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,7 +532,46 @@ class Mods(OptionSet): } -stardew_valley_option_classes = [ +@dataclass +class StardewValleyOptions(PerGameCommonOptions): + goal: Goal = Goal.default + starting_money: StartingMoney = StartingMoney.default + profit_margin: ProfitMargin = ProfitMargin.default + bundle_randomization: BundleRandomization = BundleRandomization.default + bundle_price: BundlePrice = BundlePrice.default + entrance_randomization: EntranceRandomization = EntranceRandomization.default + season_randomization: SeasonRandomization = SeasonRandomization.default + cropsanity: Cropsanity = Cropsanity.default + backpack_progression: BackpackProgression = BackpackProgression.default + tool_progression: ToolProgression = ToolProgression.default + skill_progression: SkillProgression = SkillProgression.default + building_progression: BuildingProgression = BuildingProgression.default + festival_locations: FestivalLocations = FestivalLocations.default + elevator_progression: ElevatorProgression = ElevatorProgression.default + arcade_machine_locations: ArcadeMachineLocations = ArcadeMachineLocations.default + special_order_locations: SpecialOrderLocations = SpecialOrderLocations.default + help_wanted_locations: HelpWantedLocations = HelpWantedLocations.default + fishsanity: Fishsanity = Fishsanity.default + museumsanity: Museumsanity = Museumsanity.default + friendsanity: Friendsanity = Friendsanity.default + friendsanity_heart_size: FriendsanityHeartSize = FriendsanityHeartSize.default + number_of_movement_buffs: NumberOfMovementBuffs = NumberOfMovementBuffs.default + number_of_luck_buffs: NumberOfLuckBuffs = NumberOfLuckBuffs.default + exclude_ginger_island: ExcludeGingerIsland = ExcludeGingerIsland.default + trap_items: TrapItems = TrapItems.default + multiple_day_sleep_enabled: MultipleDaySleepEnabled = MultipleDaySleepEnabled.default + multiple_day_sleep_cost: MultipleDaySleepCost = MultipleDaySleepCost.default + experience_multiplier: ExperienceMultiplier = ExperienceMultiplier.default + friendship_multiplier: FriendshipMultiplier = FriendshipMultiplier.default + debris_multiplier: DebrisMultiplier = DebrisMultiplier.default + quick_start: QuickStart = QuickStart.default + gifting: Gifting = Gifting.default + mods: Mods = Mods.default + death_link: DeathLink = DeathLink.default + + + +stardew_valley_option_classes = { Goal, StartingMoney, ProfitMargin, @@ -587,22 +605,6 @@ class Mods(OptionSet): 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 +stardew_valley_option_names = {option.internal_name for option in stardew_valley_option_classes} diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py index 60cad4c136fc..e8daa772d887 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -2,11 +2,10 @@ 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 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ] -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] diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 34ee1f807dd3..f56dec39a1f0 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -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 .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.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) - - MultiWorldRules.add_rule(multi_world.get_location("Old Master Cannoli", 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(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) - - -def set_skill_rule(logic, multi_world, player, skill: str, level: int): + 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, 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 @@ def set_special_order_rules(all_location_names: List[str], logic: StardewLogic, 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()) diff --git a/worlds/stardew_valley/test/TestLogicSimplification.py b/worlds/stardew_valley/test/TestLogicSimplification.py index 83d779ce9043..33b2428098bc 100644 --- a/worlds/stardew_valley/test/TestLogicSimplification.py +++ b/worlds/stardew_valley/test/TestLogicSimplification.py @@ -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(): diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 1cd17ada1f6a..595a165384db 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -1,14 +1,13 @@ 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, options, items_by_group, Group, StardewValleyWorld from ..locations import locations_by_tag, LocationTags, location_table -from ..options import StardewOption, stardew_valley_option_classes, Mods +from ..options import stardew_valley_option_classes from ..strings.goal_names import Goal from ..strings.season_names import Season from ..strings.special_order_names import SpecialOrder @@ -52,22 +51,24 @@ 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: + option_name = option.internal_name if not issubclass(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: + option_name = option.internal_name 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) @@ -149,28 +150,31 @@ def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase): def test_given_special_range_when_generate_exclude_ginger_island(self): for option in stardew_valley_option_classes: + option_name = option.internal_name if not issubclass(option, - SpecialRange) or option.internal_name == options.ExcludeGingerIsland.internal_name: + SpecialRange) or option_name == options.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]}) + 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: + option_name = option.internal_name + if not option.options or option_name == island_option.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: + option_name: option.options[value]}, seed) + stardew_world: StardewValleyWorld = multiworld.worlds[self.player] + if stardew_world.options.exclude_ginger_island != island_option.option_true: continue basic_checks(self, multiworld) check_no_ginger_island(self, multiworld) @@ -184,7 +188,8 @@ def test_given_island_related_goal_then_override_exclude_ginger_island(self): 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) + stardew_world: StardewValleyWorld = multiworld.worlds[self.player] + self.assertEqual(stardew_world.options.exclude_ginger_island, island_option.option_false) basic_checks(self, multiworld) diff --git a/worlds/stardew_valley/test/TestRegions.py b/worlds/stardew_valley/test/TestRegions.py index 293ce72d07cf..606cf5f12947 100644 --- a/worlds/stardew_valley/test/TestRegions.py +++ b/worlds/stardew_valley/test/TestRegions.py @@ -3,7 +3,7 @@ import unittest from . import SVTestBase, setup_solo_multiworld -from .. import StardewOptions, options, StardewValleyWorld +from .. import options, StardewValleyWorld, StardewValleyOptions from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag connections_by_name = {connection.name for connection in vanilla_connections} @@ -37,8 +37,9 @@ def test_entrance_randomization(self): 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 = StardewValleyOptions() + world_options.entrance_randomization = option + world_options.exclude_ginger_island = options.ExcludeGingerIsland.option_false regions_by_name = {region.name: region for region in vanilla_regions} _, randomized_connections = randomize_connections(rand, world_options, regions_by_name) @@ -62,8 +63,9 @@ def test_entrance_randomization_without_island(self): 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 = StardewValleyOptions() + world_options.entrance_randomization = option + world_options.exclude_ginger_island = options.ExcludeGingerIsland.option_true regions_by_name = {region.name: region for region in vanilla_regions} _, randomized_connections = randomize_connections(rand, world_options, regions_by_name) diff --git a/worlds/stardew_valley/test/long/option_names.py b/worlds/stardew_valley/test/long/option_names.py index 9bb950d3a64b..b6b9fb2a6163 100644 --- a/worlds/stardew_valley/test/long/option_names.py +++ b/worlds/stardew_valley/test/long/option_names.py @@ -1,7 +1,8 @@ -from worlds.stardew_valley.options import stardew_valley_option_classes +from ...options import stardew_valley_option_names 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] + +options_to_include = [option_name for option_name in stardew_valley_option_names + if option_name not in options_to_exclude] From 0c1b3b22a0d97d4f6de00cebe4cec7625397f174 Mon Sep 17 00:00:00 2001 From: Alex Gilbert Date: Sun, 8 Oct 2023 01:30:34 -0400 Subject: [PATCH 74/88] - I think it all works now --- Options.py | 16 ++-- worlds/stardew_valley/options.py | 69 +++++++++-------- worlds/stardew_valley/test/TestRegions.py | 17 +++-- worlds/stardew_valley/test/__init__.py | 83 +++++++++++---------- worlds/stardew_valley/test/mods/TestMods.py | 62 +++++++-------- 5 files changed, 126 insertions(+), 121 deletions(-) diff --git a/Options.py b/Options.py index cf1e1d3c4311..3bf449e18807 100644 --- a/Options.py +++ b/Options.py @@ -1076,14 +1076,14 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P @dataclass class PerGameCommonOptions(CommonOptions): - local_items: LocalItems = LocalItems.default - non_local_items: NonLocalItems = NonLocalItems.default - start_inventory: StartInventory = field(default_factory=StartInventory) - start_hints: StartHints = StartHints.default - start_location_hints: StartLocationHints = StartLocationHints.default - exclude_locations: ExcludeLocations = ExcludeLocations.default - priority_locations: PriorityLocations = PriorityLocations.default - item_links: ItemLinks = field(default_factory=ItemLinks) + 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): diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index e1486dd0a6c9..72bc770a012c 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -534,41 +534,40 @@ class Mods(OptionSet): @dataclass class StardewValleyOptions(PerGameCommonOptions): - goal: Goal = Goal.default - starting_money: StartingMoney = StartingMoney.default - profit_margin: ProfitMargin = ProfitMargin.default - bundle_randomization: BundleRandomization = BundleRandomization.default - bundle_price: BundlePrice = BundlePrice.default - entrance_randomization: EntranceRandomization = EntranceRandomization.default - season_randomization: SeasonRandomization = SeasonRandomization.default - cropsanity: Cropsanity = Cropsanity.default - backpack_progression: BackpackProgression = BackpackProgression.default - tool_progression: ToolProgression = ToolProgression.default - skill_progression: SkillProgression = SkillProgression.default - building_progression: BuildingProgression = BuildingProgression.default - festival_locations: FestivalLocations = FestivalLocations.default - elevator_progression: ElevatorProgression = ElevatorProgression.default - arcade_machine_locations: ArcadeMachineLocations = ArcadeMachineLocations.default - special_order_locations: SpecialOrderLocations = SpecialOrderLocations.default - help_wanted_locations: HelpWantedLocations = HelpWantedLocations.default - fishsanity: Fishsanity = Fishsanity.default - museumsanity: Museumsanity = Museumsanity.default - friendsanity: Friendsanity = Friendsanity.default - friendsanity_heart_size: FriendsanityHeartSize = FriendsanityHeartSize.default - number_of_movement_buffs: NumberOfMovementBuffs = NumberOfMovementBuffs.default - number_of_luck_buffs: NumberOfLuckBuffs = NumberOfLuckBuffs.default - exclude_ginger_island: ExcludeGingerIsland = ExcludeGingerIsland.default - trap_items: TrapItems = TrapItems.default - multiple_day_sleep_enabled: MultipleDaySleepEnabled = MultipleDaySleepEnabled.default - multiple_day_sleep_cost: MultipleDaySleepCost = MultipleDaySleepCost.default - experience_multiplier: ExperienceMultiplier = ExperienceMultiplier.default - friendship_multiplier: FriendshipMultiplier = FriendshipMultiplier.default - debris_multiplier: DebrisMultiplier = DebrisMultiplier.default - quick_start: QuickStart = QuickStart.default - gifting: Gifting = Gifting.default - mods: Mods = Mods.default - death_link: DeathLink = DeathLink.default - + 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 stardew_valley_option_classes = { diff --git a/worlds/stardew_valley/test/TestRegions.py b/worlds/stardew_valley/test/TestRegions.py index 606cf5f12947..2347ca33db05 100644 --- a/worlds/stardew_valley/test/TestRegions.py +++ b/worlds/stardew_valley/test/TestRegions.py @@ -4,6 +4,7 @@ from . import SVTestBase, setup_solo_multiworld 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,12 +38,12 @@ def test_entrance_randomization(self): seed = random.randrange(sys.maxsize) with self.subTest(flag=flag, msg=f"Seed: {seed}"): rand = random.Random(seed) - world_options = StardewValleyOptions() - world_options.entrance_randomization = option - world_options.exclude_ginger_island = 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: @@ -63,12 +64,12 @@ def test_entrance_randomization_without_island(self): with self.subTest(option=option, flag=flag): seed = random.randrange(sys.maxsize) rand = random.Random(seed) - world_options = StardewValleyOptions() - world_options.entrance_randomization = option - world_options.exclude_ginger_island = 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: diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 0ad6a0ebcad4..53181154d391 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -2,12 +2,15 @@ from argparse import Namespace from typing import Dict, FrozenSet, Tuple, Any, ClassVar -from BaseClasses import MultiWorld, CollectionState +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 @@ def run_default_tests(self) -> bool: 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 @@ def allsanity_options_with_mods(self): 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 = {} diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index a3198e4d2a24..8b3f49879c62 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -4,21 +4,22 @@ import sys 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 import options, locations, items, Group, ItemClassification, StardewOptions -from worlds.stardew_valley.mods.mod_data import ModNames +from worlds.stardew_valley import items, Group, ItemClassification 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 worlds.stardew_valley.locations import location_table +from worlds.stardew_valley.options import stardew_valley_option_classes, 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"] +mod_list = (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): @@ -81,10 +82,10 @@ def test_given_mod_names_when_generate_paired_with_other_options_then_basic_chec 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: mod_list } def test_all_progression_items_are_added_to_the_pool(self): @@ -105,10 +106,10 @@ def test_all_progression_items_are_added_to_the_pool(self): 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: mod_list } def test_all_progression_items_except_island_are_added_to_the_pool(self): @@ -134,20 +135,22 @@ 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: mod_list} + 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: @@ -164,12 +167,11 @@ def test_mod_entrance_randomization(self): 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()] From db38f114ef3a0f6a368dd3b98d416d343c7240c9 Mon Sep 17 00:00:00 2001 From: Alex Gilbert Date: Sun, 8 Oct 2023 01:31:36 -0400 Subject: [PATCH 75/88] - Removed last trace of me touching core --- Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Options.py b/Options.py index 3bf449e18807..d9ddfc2e2fdb 100644 --- a/Options.py +++ b/Options.py @@ -3,7 +3,7 @@ import abc import logging from copy import deepcopy -from dataclasses import dataclass, field +from dataclasses import dataclass import functools import math import numbers From 5eb17a16d44f36e2b6e02c503199f7441b18cdab Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 8 Oct 2023 02:52:14 -0500 Subject: [PATCH 76/88] typo --- worlds/overcooked2/Options.py | 2 +- worlds/overcooked2/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/overcooked2/Options.py b/worlds/overcooked2/Options.py index f58ed3aa4c23..18a2c18ed4f1 100644 --- a/worlds/overcooked2/Options.py +++ b/worlds/overcooked2/Options.py @@ -188,7 +188,7 @@ class OC2Options(PerGameCommonOptions): fix_bugs: FixBugs shorter_level_duration: ShorterLevelDuration short_horde_levels: ShortHordeLevels - always_preserver_cooking_progress: AlwaysPreserveCookingProgress + always_preserve_cooking_progress: AlwaysPreserveCookingProgress always_serve_oldest_order: AlwaysServeOldestOrder display_leaderboard_scores: DisplayLeaderboardScores diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index 688e23f75d3a..2bf523b347c8 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -575,7 +575,7 @@ def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]: bugs = ["FixDoubleServing", "FixSinkBug", "FixControlStickThrowBug", "FixEmptyBurnerThrow"] for bug in bugs: base_data[bug] = self.options.fix_bugs.result - base_data["PreserveCookingProgress"] = self.options.always_preserver_cooking_progress.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"] = { From 16ae3ad5602cc51901534188422831fce4576a45 Mon Sep 17 00:00:00 2001 From: Alex Gilbert Date: Sun, 8 Oct 2023 11:44:22 -0400 Subject: [PATCH 77/88] It now passes all tests! --- worlds/stardew_valley/test/mods/TestMods.py | 34 ++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 8b3f49879c62..6a76f0478b7b 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -14,12 +14,12 @@ from worlds.stardew_valley.options import stardew_valley_option_classes, Mods, EntranceRandomization, Friendsanity, SeasonRandomization, SpecialOrderLocations, \ ExcludeGingerIsland, TrapItems -mod_list = (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) +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): @@ -38,7 +38,7 @@ 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) @@ -47,11 +47,11 @@ def test_given_single_mods_when_generate_then_basic_checks(self): def test_given_mod_pairs_when_generate_then_basic_checks(self): if self.skip_long_tests: return - num_mods = len(mod_list) + num_mods = len(all_mods) 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] + mod1 = all_mods[mod1_index] + mod2 = all_mods[mod2_index] mods = (mod1, mod2) with self.subTest(f"Mods: {mods}"): multiworld = setup_solo_multiworld({Mods: mods}) @@ -60,7 +60,7 @@ def test_given_mod_pairs_when_generate_then_basic_checks(self): 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) @@ -73,7 +73,7 @@ def test_given_mod_names_when_generate_paired_with_other_options_then_basic_chec if not option.options: continue for value in option.options: - for mod in mod_list: + 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) @@ -85,7 +85,7 @@ class TestBaseItemGeneration(SVTestBase): Friendsanity.internal_name: Friendsanity.option_all_with_marriage, SeasonRandomization.internal_name: SeasonRandomization.option_progressive, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, - Mods.internal_name: mod_list + Mods.internal_name: all_mods } def test_all_progression_items_are_added_to_the_pool(self): @@ -109,7 +109,7 @@ class TestNoGingerIslandModItemGeneration(SVTestBase): Friendsanity.internal_name: Friendsanity.option_all_with_marriage, SeasonRandomization.internal_name: SeasonRandomization.option_progressive, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - Mods.internal_name: mod_list + Mods.internal_name: all_mods } def test_all_progression_items_except_island_are_added_to_the_pool(self): @@ -143,7 +143,7 @@ def test_mod_entrance_randomization(self): rand = random.Random(seed) world_options = {EntranceRandomization.internal_name: option, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, - Mods.internal_name: mod_list} + Mods.internal_name: all_mods} multiworld = setup_solo_multiworld(world_options) world = multiworld.worlds[1] final_regions = create_final_regions(world.options) @@ -157,9 +157,9 @@ def test_mod_entrance_randomization(self): 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}") From 228dfaae60f5030a4f62ac81b1ec26e25f3562b0 Mon Sep 17 00:00:00 2001 From: Alex Gilbert Date: Sun, 8 Oct 2023 21:32:47 -0400 Subject: [PATCH 78/88] Improve options, fix all issues I hope --- worlds/stardew_valley/__init__.py | 2 +- worlds/stardew_valley/options.py | 79 ++++++++-------- worlds/stardew_valley/test/TestOptions.py | 94 ++++++++++--------- .../test/checks/option_checks.py | 4 +- .../stardew_valley/test/long/TestModsLong.py | 63 +++++++++++++ .../test/long/TestOptionsLong.py | 7 +- .../test/long/TestRandomWorlds.py | 9 +- .../stardew_valley/test/long/option_names.py | 6 +- worlds/stardew_valley/test/mods/TestMods.py | 44 ++------- 9 files changed, 174 insertions(+), 134 deletions(-) create mode 100644 worlds/stardew_valley/test/long/TestModsLong.py diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 1d367654d666..6d805cdf8995 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -86,7 +86,7 @@ def force_change_options_if_incompatible(self): goal_is_island_related = goal_is_walnut_hunter or goal_is_perfection exclude_ginger_island = self.options.exclude_ginger_island == options.ExcludeGingerIsland.option_true if goal_is_island_related and exclude_ginger_island: - self.options.exclude_ginger_island = options.ExcludeGingerIsland.option_false + self.options.exclude_ginger_island.value = options.ExcludeGingerIsland.option_false goal_name = self.options.goal.current_key player_name = self.multiworld.player_name[self.player] logging.warning(f"Goal '{goal_name}' requires Ginger Island. Exclude Ginger Island setting forced to 'False' for player {self.player} ({player_name})") diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index 72bc770a012c..e008f622b8e3 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -1,6 +1,7 @@ from dataclasses import dataclass +from typing import Dict -from Options import Range, SpecialRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink +from Options import Range, SpecialRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, Option from .mods.mod_data import ModNames @@ -569,41 +570,41 @@ class StardewValleyOptions(PerGameCommonOptions): mods: Mods death_link: DeathLink - -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_option_names = {option.internal_name for option in stardew_valley_option_classes} + def get_option_value(self, name: str) -> int: + return self.get_options_by_name()[name].value + + def get_options_by_name(self): + return { + self.goal.internal_name: self.goal, + self.starting_money.internal_name: self.starting_money, + self.profit_margin.internal_name: self.profit_margin, + self.bundle_randomization.internal_name: self.bundle_randomization, + self.bundle_price.internal_name: self.bundle_price, + self.entrance_randomization.internal_name: self.entrance_randomization, + self.season_randomization.internal_name: self.season_randomization, + self.cropsanity.internal_name: self.cropsanity, + self.backpack_progression.internal_name: self.backpack_progression, + self.tool_progression.internal_name: self.tool_progression, + self.skill_progression.internal_name: self.skill_progression, + self.building_progression.internal_name: self.building_progression, + self.festival_locations.internal_name: self.festival_locations, + self.elevator_progression.internal_name: self.elevator_progression, + self.arcade_machine_locations.internal_name: self.arcade_machine_locations, + self.special_order_locations.internal_name: self.special_order_locations, + self.help_wanted_locations.internal_name: self.help_wanted_locations, + self.fishsanity.internal_name: self.fishsanity, + self.museumsanity.internal_name: self.museumsanity, + self.friendsanity.internal_name: self.friendsanity, + self.friendsanity_heart_size.internal_name: self.friendsanity_heart_size, + self.number_of_movement_buffs.internal_name: self.number_of_movement_buffs, + self.number_of_luck_buffs.internal_name: self.number_of_luck_buffs, + self.exclude_ginger_island.internal_name: self.exclude_ginger_island, + self.trap_items.internal_name: self.trap_items, + self.multiple_day_sleep_enabled.internal_name: self.multiple_day_sleep_enabled, + self.multiple_day_sleep_cost.internal_name: self.multiple_day_sleep_cost, + self.experience_multiplier.internal_name: self.experience_multiplier, + self.friendship_multiplier.internal_name: self.friendship_multiplier, + self.debris_multiplier.internal_name: self.debris_multiplier, + self.quick_start.internal_name: self.quick_start, + self.gifting.internal_name: self.gifting, + } diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 595a165384db..0dd83010c6c4 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -5,10 +5,10 @@ from BaseClasses import ItemClassification, MultiWorld from Options import SpecialRange from . import setup_solo_multiworld, SVTestBase -from .. import StardewItem, options, items_by_group, Group, StardewValleyWorld +from .. import StardewItem, items_by_group, Group, StardewValleyWorld from ..locations import locations_by_tag, LocationTags, location_table -from ..options import stardew_valley_option_classes -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 @@ -50,9 +50,10 @@ 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: - option_name = option.internal_name - if not issubclass(option, SpecialRange): + options = self.world.options.get_options_by_name() + for option_name in options: + option = options[option_name] + if not isinstance(option, SpecialRange): continue for value in option.special_range_names: with self.subTest(f"{option_name}: {value}"): @@ -62,8 +63,9 @@ def test_given_special_range_when_generate_then_basic_checks(self): def test_given_choice_when_generate_then_basic_checks(self): seed = int(random() * pow(10, 18) - 1) - for option in stardew_valley_option_classes: - option_name = option.internal_name + options = self.world.options.get_options_by_name() + for option_name in options: + option = options[option_name] if not option.options: continue for value in option.options: @@ -75,16 +77,16 @@ def test_given_choice_when_generate_then_basic_checks(self): 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) @@ -92,14 +94,14 @@ def test_given_goal_when_generate_then_victory_is_in_correct_location(self): 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 @@ -107,7 +109,7 @@ def test_given_randomized_when_generate_then_all_seasons_are_in_the_pool_or_prec 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()] @@ -116,7 +118,7 @@ def test_given_progressive_when_generate_then_3_progressive_seasons_are_in_the_p 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()} @@ -124,7 +126,7 @@ def test_given_vanilla_when_generate_then_no_tool_in_pool(self): 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()] @@ -132,7 +134,7 @@ def test_given_progressive_when_generate_then_progressive_tool_of_each_is_in_poo 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)} @@ -149,44 +151,44 @@ def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase): def test_given_special_range_when_generate_exclude_ginger_island(self): - for option in stardew_valley_option_classes: - option_name = option.internal_name - if not issubclass(option, - SpecialRange) or option_name == options.ExcludeGingerIsland.internal_name: + options = self.world.options.get_options_by_name() + for option_name in options: + option = options[option_name] + if not isinstance(option, SpecialRange) or option_name == ExcludeGingerIsland.internal_name: continue for value in option.special_range_names: with self.subTest(f"{option_name}: {value}"): multiworld = setup_solo_multiworld( - {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + {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: - option_name = option.internal_name - if not option.options or option_name == island_option.internal_name: + options = self.world.options.get_options_by_name() + for option_name in options: + option = options[option_name] + if not option.options or option_name == ExcludeGingerIsland.internal_name: continue for value in option.options: with self.subTest(f"{option_name}: {value} [Seed: {seed}]"): multiworld = setup_solo_multiworld( - {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 != island_option.option_true: + 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]}) stardew_world: StardewValleyWorld = multiworld.worlds[self.player] self.assertEqual(stardew_world.options.exclude_ginger_island, island_option.option_false) @@ -196,7 +198,7 @@ def test_given_island_related_goal_then_override_exclude_ginger_island(self): 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]] @@ -207,12 +209,12 @@ def test_given_no_traps_when_generate_then_no_trap_in_pool(self): 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()] @@ -223,7 +225,7 @@ def test_given_traps_when_generate_then_all_traps_in_pool(self): 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} @@ -233,7 +235,7 @@ def test_given_disabled_then_no_order_in_pool(self): 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} @@ -247,8 +249,8 @@ def test_given_board_only_then_no_qi_order_in_pool(self): 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()} @@ -263,8 +265,8 @@ def test_given_board_and_qi_then_all_orders_in_pool(self): 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()} diff --git a/worlds/stardew_valley/test/checks/option_checks.py b/worlds/stardew_valley/test/checks/option_checks.py index e6bced5b1ce7..35e9e6156e0b 100644 --- a/worlds/stardew_valley/test/checks/option_checks.py +++ b/worlds/stardew_valley/test/checks/option_checks.py @@ -20,7 +20,7 @@ def is_setting(multiworld: MultiWorld, setting_name: str, setting_value: int) -> stardew_world = get_stardew_world(multiworld) if not stardew_world: return False - current_value = stardew_world.options[setting_name] + current_value = stardew_world.options.get_option_value(setting_name) return current_value == setting_value @@ -32,7 +32,7 @@ def assert_is_setting(tester: SVTestBase, multiworld: MultiWorld, setting_name: stardew_world = get_stardew_world(multiworld) if not stardew_world: return False - current_value = stardew_world.options[setting_name] + current_value = stardew_world.options.get_option_value(setting_name) tester.assertEqual(current_value, setting_value) diff --git a/worlds/stardew_valley/test/long/TestModsLong.py b/worlds/stardew_valley/test/long/TestModsLong.py new file mode 100644 index 000000000000..b3ec6f1420ad --- /dev/null +++ b/worlds/stardew_valley/test/long/TestModsLong.py @@ -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) \ No newline at end of file diff --git a/worlds/stardew_valley/test/long/TestOptionsLong.py b/worlds/stardew_valley/test/long/TestOptionsLong.py index c614ddcc36fa..6de9c0820bae 100644 --- a/worlds/stardew_valley/test/long/TestOptionsLong.py +++ b/worlds/stardew_valley/test/long/TestOptionsLong.py @@ -24,10 +24,12 @@ class TestGenerateDynamicOptions(SVTestBase): def test_given_option_pair_when_generate_then_basic_checks(self): if self.skip_long_tests: return - + counter = 0 num_options = len(options_to_include) for option1_index in range(0, num_options): for option2_index in range(option1_index + 1, num_options): + if counter > 100: + return option1 = options_to_include[option1_index] option2 = options_to_include[option2_index] option1_choices = get_option_choices(option1) @@ -38,4 +40,5 @@ def test_given_option_pair_when_generate_then_basic_checks(self): choices = {option1.internal_name: option1_choices[key1], option2.internal_name: option2_choices[key2]} multiworld = setup_solo_multiworld(choices) - basic_checks(self, multiworld) \ No newline at end of file + basic_checks(self, multiworld) + counter = counter + 1 \ No newline at end of file diff --git a/worlds/stardew_valley/test/long/TestRandomWorlds.py b/worlds/stardew_valley/test/long/TestRandomWorlds.py index 6ba814aba2c4..e4d4350a3df7 100644 --- a/worlds/stardew_valley/test/long/TestRandomWorlds.py +++ b/worlds/stardew_valley/test/long/TestRandomWorlds.py @@ -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.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]: @@ -91,9 +90,9 @@ def check_multiworld_is_valid(tester: SVTestBase, multiworld_id: int, multiworld class TestGenerateManyWorlds(SVTestBase): def test_generate_many_worlds_then_check_results(self): - if self.skip_long_tests: - return - number_worlds = 1000 + # if self.skip_long_tests: + # return + number_worlds = 100 start_index = random.Random().randint(0, 9999999999) multiworlds = generate_many_worlds(number_worlds, start_index) check_every_multiworld_is_valid(self, multiworlds) diff --git a/worlds/stardew_valley/test/long/option_names.py b/worlds/stardew_valley/test/long/option_names.py index b6b9fb2a6163..649d0da5b33f 100644 --- a/worlds/stardew_valley/test/long/option_names.py +++ b/worlds/stardew_valley/test/long/option_names.py @@ -1,8 +1,8 @@ -from ...options import stardew_valley_option_names +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"] + "quick_start", "gifting", "gift_tax", "progression_balancing", "accessibility", "start_inventory", "start_hints", "death_link"] -options_to_include = [option_name for option_name in stardew_valley_option_names +options_to_include = [option for option_name, option in StardewValleyWorld.options_dataclass.type_hints.items() if option_name not in options_to_exclude] diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 6a76f0478b7b..02fd30a6b11f 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -4,15 +4,14 @@ import sys 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 import items, Group, ItemClassification -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 -from worlds.stardew_valley.options import stardew_valley_option_classes, Mods, EntranceRandomization, Friendsanity, SeasonRandomization, SpecialOrderLocations, \ - ExcludeGingerIsland, TrapItems +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 all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, @@ -44,20 +43,6 @@ def test_given_single_mods_when_generate_then_basic_checks(self): 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(all_mods) - for mod1_index in range(0, num_mods): - for mod2_index in range(mod1_index + 1, num_mods): - mod1 = all_mods[mod1_index] - mod2 = all_mods[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 all_mods: @@ -66,19 +51,6 @@ def test_given_mod_names_when_generate_paired_with_entrance_randomizer_then_basi 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 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) - class TestBaseItemGeneration(SVTestBase): options = { From 92f08a7a10dd2ec638ebae826c2af4a778b6c1df Mon Sep 17 00:00:00 2001 From: Alex Gilbert Date: Sun, 8 Oct 2023 23:59:17 -0400 Subject: [PATCH 79/88] - Fixed init options --- worlds/stardew_valley/__init__.py | 67 ++++++++++++++++--------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 6d805cdf8995..1f94e9e43080 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -8,11 +8,12 @@ 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 StardewValleyOptions +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 @@ -81,12 +82,12 @@ def generate_early(self): self.options.bundle_price) def force_change_options_if_incompatible(self): - goal_is_walnut_hunter = self.options.goal == options.Goal.option_greatest_walnut_hunter - goal_is_perfection = self.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.exclude_ginger_island == 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.exclude_ginger_island.value = options.ExcludeGingerIsland.option_false + 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_name}' requires Ginger Island. Exclude Ginger Island setting forced to 'False' for player {self.player} ({player_name})") @@ -115,7 +116,7 @@ def create_items(self): if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK, Group.FRIENDSHIP_PACK)] - if self.options.season_randomization == 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]] @@ -133,12 +134,12 @@ def create_items(self): self.setup_victory() def precollect_starting_season(self) -> Optional[StardewItem]: - if self.options.season_randomization == options.SeasonRandomization.option_progressive: + if self.options.season_randomization == SeasonRandomization.option_progressive: return season_pool = items_by_group[Group.SEASON] - if self.options.season_randomization == 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 @@ -147,7 +148,7 @@ def precollect_starting_season(self) -> Optional[StardewItem]: if item.name in {season.name for season in items_by_group[Group.SEASON]}]: return - if self.options.season_randomization == 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)) @@ -155,10 +156,10 @@ def precollect_starting_season(self) -> Optional[StardewItem]: def setup_early_items(self): if (self.options.building_progression == - options.BuildingProgression.option_progressive_early_shipping_bin): + BuildingProgression.option_progressive_early_shipping_bin): self.multiworld.early_items[self.player]["Shipping Bin"] = 1 - if self.options.backpack_progression == 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): @@ -171,40 +172,40 @@ def setup_month_events(self): self.create_event_location(month_end, self.logic.received("Month End", i).simplify(), "Month End") def setup_victory(self): - if self.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.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.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.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.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.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.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.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.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") @@ -275,9 +276,9 @@ def fill_slot_data(self) -> Dict[str, Any]: 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.as_dict()) + excluded_options = [BundleRandomization, BundlePrice, + NumberOfMovementBuffs, NumberOfLuckBuffs] + slot_data = dict(self.options.get_options_by_name()) for option in excluded_options: slot_data.pop(option.internal_name) slot_data.update({ From 095195a542d204d0ec703d15bfef16e0ead6bac7 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 9 Oct 2023 12:19:57 -0500 Subject: [PATCH 80/88] dlcquest: fix bad imports --- worlds/dlcquest/Items.py | 7 ++++--- worlds/dlcquest/Locations.py | 3 +-- worlds/dlcquest/Regions.py | 5 +++-- worlds/dlcquest/Rules.py | 6 +++--- worlds/dlcquest/__init__.py | 15 ++++++++------- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/worlds/dlcquest/Items.py b/worlds/dlcquest/Items.py index 61f4cd30fabf..c7e72edd5b5c 100644 --- a/worlds/dlcquest/Items.py +++ b/worlds/dlcquest/Items.py @@ -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): diff --git a/worlds/dlcquest/Locations.py b/worlds/dlcquest/Locations.py index 08d37e781216..a9fdd00a202c 100644 --- a/worlds/dlcquest/Locations.py +++ b/worlds/dlcquest/Locations.py @@ -1,5 +1,4 @@ -from BaseClasses import Location, MultiWorld -from . import Options +from BaseClasses import Location class DLCQuestLocation(Location): diff --git a/worlds/dlcquest/Regions.py b/worlds/dlcquest/Regions.py index 5dc094b8fcfb..dfb5f6c021be 100644 --- a/worlds/dlcquest/Regions.py +++ b/worlds/dlcquest/Regions.py @@ -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"] diff --git a/worlds/dlcquest/Rules.py b/worlds/dlcquest/Rules.py index f9e99fd893ea..c5fdfe8282c4 100644 --- a/worlds/dlcquest/Rules.py +++ b/worlds/dlcquest/Rules.py @@ -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): diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index f3e10f26d6aa..392eac7796fb 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -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 From f5c0e23c86d6012b33dd4c60e946b9530337a17c Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 9 Oct 2023 13:22:44 -0500 Subject: [PATCH 81/88] missed a file --- worlds/dlcquest/Items.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/worlds/dlcquest/Items.py b/worlds/dlcquest/Items.py index c7e72edd5b5c..61d1be54cbd4 100644 --- a/worlds/dlcquest/Items.py +++ b/worlds/dlcquest/Items.py @@ -94,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) From c38cce9686c08d6595d8a9eb70837d53208fca36 Mon Sep 17 00:00:00 2001 From: Alex Gilbert Date: Mon, 9 Oct 2023 15:42:29 -0400 Subject: [PATCH 82/88] - Reduce code duplication --- worlds/stardew_valley/__init__.py | 5 ++- worlds/stardew_valley/options.py | 39 ------------------- worlds/stardew_valley/test/TestOptions.py | 8 ++-- .../test/checks/option_checks.py | 4 +- .../test/long/TestOptionsLong.py | 6 +-- .../test/long/TestRandomWorlds.py | 6 +-- 6 files changed, 13 insertions(+), 55 deletions(-) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 1f94e9e43080..b2b784e18014 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -1,5 +1,5 @@ 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 worlds.AutoWorld import World, WebWorld @@ -278,7 +278,8 @@ def fill_slot_data(self) -> Dict[str, Any]: excluded_options = [BundleRandomization, BundlePrice, NumberOfMovementBuffs, NumberOfLuckBuffs] - slot_data = dict(self.options.get_options_by_name()) + included_option_names: List[str] = [option_name for option_name in self.options_dataclass.type_hints if option_name not in excluded_options] + slot_data = self.options.as_dict(*included_option_names) for option in excluded_options: slot_data.pop(option.internal_name) slot_data.update({ diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index e008f622b8e3..75573359a5ab 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -569,42 +569,3 @@ class StardewValleyOptions(PerGameCommonOptions): gifting: Gifting mods: Mods death_link: DeathLink - - def get_option_value(self, name: str) -> int: - return self.get_options_by_name()[name].value - - def get_options_by_name(self): - return { - self.goal.internal_name: self.goal, - self.starting_money.internal_name: self.starting_money, - self.profit_margin.internal_name: self.profit_margin, - self.bundle_randomization.internal_name: self.bundle_randomization, - self.bundle_price.internal_name: self.bundle_price, - self.entrance_randomization.internal_name: self.entrance_randomization, - self.season_randomization.internal_name: self.season_randomization, - self.cropsanity.internal_name: self.cropsanity, - self.backpack_progression.internal_name: self.backpack_progression, - self.tool_progression.internal_name: self.tool_progression, - self.skill_progression.internal_name: self.skill_progression, - self.building_progression.internal_name: self.building_progression, - self.festival_locations.internal_name: self.festival_locations, - self.elevator_progression.internal_name: self.elevator_progression, - self.arcade_machine_locations.internal_name: self.arcade_machine_locations, - self.special_order_locations.internal_name: self.special_order_locations, - self.help_wanted_locations.internal_name: self.help_wanted_locations, - self.fishsanity.internal_name: self.fishsanity, - self.museumsanity.internal_name: self.museumsanity, - self.friendsanity.internal_name: self.friendsanity, - self.friendsanity_heart_size.internal_name: self.friendsanity_heart_size, - self.number_of_movement_buffs.internal_name: self.number_of_movement_buffs, - self.number_of_luck_buffs.internal_name: self.number_of_luck_buffs, - self.exclude_ginger_island.internal_name: self.exclude_ginger_island, - self.trap_items.internal_name: self.trap_items, - self.multiple_day_sleep_enabled.internal_name: self.multiple_day_sleep_enabled, - self.multiple_day_sleep_cost.internal_name: self.multiple_day_sleep_cost, - self.experience_multiplier.internal_name: self.experience_multiplier, - self.friendship_multiplier.internal_name: self.friendship_multiplier, - self.debris_multiplier.internal_name: self.debris_multiplier, - self.quick_start.internal_name: self.quick_start, - self.gifting.internal_name: self.gifting, - } diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 0dd83010c6c4..44541895468c 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -50,7 +50,7 @@ def get_option_choices(option) -> Dict[str, int]: class TestGenerateDynamicOptions(SVTestBase): def test_given_special_range_when_generate_then_basic_checks(self): - options = self.world.options.get_options_by_name() + options = self.world.options_dataclass.type_hints for option_name in options: option = options[option_name] if not isinstance(option, SpecialRange): @@ -63,7 +63,7 @@ def test_given_special_range_when_generate_then_basic_checks(self): def test_given_choice_when_generate_then_basic_checks(self): seed = int(random() * pow(10, 18) - 1) - options = self.world.options.get_options_by_name() + options = self.world.options_dataclass.type_hints for option_name in options: option = options[option_name] if not option.options: @@ -151,7 +151,7 @@ def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase): def test_given_special_range_when_generate_exclude_ginger_island(self): - options = self.world.options.get_options_by_name() + options = self.world.options_dataclass.type_hints for option_name in options: option = options[option_name] if not isinstance(option, SpecialRange) or option_name == ExcludeGingerIsland.internal_name: @@ -165,7 +165,7 @@ def test_given_special_range_when_generate_exclude_ginger_island(self): def test_given_choice_when_generate_exclude_ginger_island(self): seed = int(random() * pow(10, 18) - 1) - options = self.world.options.get_options_by_name() + options = self.world.options_dataclass.type_hints for option_name in options: option = options[option_name] if not option.options or option_name == ExcludeGingerIsland.internal_name: diff --git a/worlds/stardew_valley/test/checks/option_checks.py b/worlds/stardew_valley/test/checks/option_checks.py index 35e9e6156e0b..6f45f2cf724c 100644 --- a/worlds/stardew_valley/test/checks/option_checks.py +++ b/worlds/stardew_valley/test/checks/option_checks.py @@ -20,7 +20,7 @@ def is_setting(multiworld: MultiWorld, setting_name: str, setting_value: int) -> stardew_world = get_stardew_world(multiworld) if not stardew_world: return False - current_value = stardew_world.options.get_option_value(setting_name) + current_value = stardew_world.options.as_dict(setting_name)[setting_name] return current_value == setting_value @@ -32,7 +32,7 @@ def assert_is_setting(tester: SVTestBase, multiworld: MultiWorld, setting_name: stardew_world = get_stardew_world(multiworld) if not stardew_world: return False - current_value = stardew_world.options.get_option_value(setting_name) + current_value = stardew_world.options.as_dict(setting_name)[setting_name] tester.assertEqual(current_value, setting_value) diff --git a/worlds/stardew_valley/test/long/TestOptionsLong.py b/worlds/stardew_valley/test/long/TestOptionsLong.py index 6de9c0820bae..23ac6125e64f 100644 --- a/worlds/stardew_valley/test/long/TestOptionsLong.py +++ b/worlds/stardew_valley/test/long/TestOptionsLong.py @@ -24,12 +24,9 @@ class TestGenerateDynamicOptions(SVTestBase): def test_given_option_pair_when_generate_then_basic_checks(self): if self.skip_long_tests: return - counter = 0 num_options = len(options_to_include) for option1_index in range(0, num_options): for option2_index in range(option1_index + 1, num_options): - if counter > 100: - return option1 = options_to_include[option1_index] option2 = options_to_include[option2_index] option1_choices = get_option_choices(option1) @@ -40,5 +37,4 @@ def test_given_option_pair_when_generate_then_basic_checks(self): choices = {option1.internal_name: option1_choices[key1], option2.internal_name: option2_choices[key2]} multiworld = setup_solo_multiworld(choices) - basic_checks(self, multiworld) - counter = counter + 1 \ No newline at end of file + basic_checks(self, multiworld) \ No newline at end of file diff --git a/worlds/stardew_valley/test/long/TestRandomWorlds.py b/worlds/stardew_valley/test/long/TestRandomWorlds.py index e4d4350a3df7..0145f471d100 100644 --- a/worlds/stardew_valley/test/long/TestRandomWorlds.py +++ b/worlds/stardew_valley/test/long/TestRandomWorlds.py @@ -90,9 +90,9 @@ def check_multiworld_is_valid(tester: SVTestBase, multiworld_id: int, multiworld class TestGenerateManyWorlds(SVTestBase): def test_generate_many_worlds_then_check_results(self): - # if self.skip_long_tests: - # return - number_worlds = 100 + if self.skip_long_tests: + return + number_worlds = 1000 start_index = random.Random().randint(0, 9999999999) multiworlds = generate_many_worlds(number_worlds, start_index) check_every_multiworld_is_valid(self, multiworlds) From 4876639477dc57e6a13300b10cd0c119bd72b8f4 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 9 Oct 2023 15:55:27 -0500 Subject: [PATCH 83/88] add as_dict documentation --- docs/world api.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/world api.md b/docs/world api.md index 896ca209d73d..a50bef648cd4 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -712,6 +712,17 @@ def generate_output(self, output_directory: str): generate_mod(src, out_file, data) ``` +### Slot Data + +```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 From b0f7671821702354091dec015e9a13822e193657 Mon Sep 17 00:00:00 2001 From: Alex Gilbert Date: Tue, 10 Oct 2023 00:30:56 -0400 Subject: [PATCH 84/88] - Use .items(), get option name more directly, fix slot data content --- Options.py | 8 ++++++++ worlds/stardew_valley/__init__.py | 8 +++----- worlds/stardew_valley/test/TestOptions.py | 12 ++++-------- worlds/stardew_valley/test/checks/option_checks.py | 4 ++-- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Options.py b/Options.py index d9ddfc2e2fdb..da2723d42a67 100644 --- a/Options.py +++ b/Options.py @@ -955,6 +955,14 @@ def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}") return option_results + def get_value(self, option_name: str) -> typing.Any: + """ + Returns the value of a given option + + :param option_name: names of the option to return the value of + """ + return getattr(self, option_name).value + class LocalItems(ItemSet): """Forces these items to be in their native world.""" diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index b2b784e18014..2e640f3407a3 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -276,12 +276,10 @@ def fill_slot_data(self) -> Dict[str, Any]: key, value = self.modified_bundles[bundle_key].to_pair() modified_bundles[key] = value - excluded_options = [BundleRandomization, BundlePrice, - NumberOfMovementBuffs, NumberOfLuckBuffs] - included_option_names: List[str] = [option_name for option_name in self.options_dataclass.type_hints if option_name not in excluded_options] + excluded_options = [BundleRandomization, BundlePrice, NumberOfMovementBuffs, NumberOfLuckBuffs] + excluded_option_names = [option.internal_name for option in excluded_options] + 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) - for option in excluded_options: - slot_data.pop(option.internal_name) slot_data.update({ "seed": self.multiworld.per_slot_randoms[self.player].randrange(1000000000), # Seed should be max 9 digits "randomized_entrances": self.randomized_entrances, diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 44541895468c..712aa300d537 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -51,8 +51,7 @@ def get_option_choices(option) -> Dict[str, int]: class TestGenerateDynamicOptions(SVTestBase): def test_given_special_range_when_generate_then_basic_checks(self): options = self.world.options_dataclass.type_hints - for option_name in options: - option = options[option_name] + for option_name, option in options.items(): if not isinstance(option, SpecialRange): continue for value in option.special_range_names: @@ -64,8 +63,7 @@ def test_given_special_range_when_generate_then_basic_checks(self): def test_given_choice_when_generate_then_basic_checks(self): seed = int(random() * pow(10, 18) - 1) options = self.world.options_dataclass.type_hints - for option_name in options: - option = options[option_name] + for option_name, option in options.items(): if not option.options: continue for value in option.options: @@ -152,8 +150,7 @@ def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase): def test_given_special_range_when_generate_exclude_ginger_island(self): options = self.world.options_dataclass.type_hints - for option_name in options: - option = options[option_name] + 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: @@ -166,8 +163,7 @@ def test_given_special_range_when_generate_exclude_ginger_island(self): def test_given_choice_when_generate_exclude_ginger_island(self): seed = int(random() * pow(10, 18) - 1) options = self.world.options_dataclass.type_hints - for option_name in options: - option = options[option_name] + for option_name, option in options.items(): if not option.options or option_name == ExcludeGingerIsland.internal_name: continue for value in option.options: diff --git a/worlds/stardew_valley/test/checks/option_checks.py b/worlds/stardew_valley/test/checks/option_checks.py index 6f45f2cf724c..06a97b8a256f 100644 --- a/worlds/stardew_valley/test/checks/option_checks.py +++ b/worlds/stardew_valley/test/checks/option_checks.py @@ -20,7 +20,7 @@ def is_setting(multiworld: MultiWorld, setting_name: str, setting_value: int) -> stardew_world = get_stardew_world(multiworld) if not stardew_world: return False - current_value = stardew_world.options.as_dict(setting_name)[setting_name] + current_value = stardew_world.options.get_value(setting_name) return current_value == setting_value @@ -32,7 +32,7 @@ def assert_is_setting(tester: SVTestBase, multiworld: MultiWorld, setting_name: stardew_world = get_stardew_world(multiworld) if not stardew_world: return False - current_value = stardew_world.options.as_dict(setting_name)[setting_name] + current_value = stardew_world.options.get_value(setting_name) tester.assertEqual(current_value, setting_value) From 82ae9dfdde0003f22b9c32f1fb9b3ad1a969a807 Mon Sep 17 00:00:00 2001 From: Alex Gilbert Date: Tue, 10 Oct 2023 00:42:38 -0400 Subject: [PATCH 85/88] - Remove generic options from the slot data --- worlds/stardew_valley/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 2e640f3407a3..1f46eb79d79a 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -2,6 +2,7 @@ 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 from .bundles import get_all_bundles, Bundle @@ -278,6 +279,8 @@ def fill_slot_data(self) -> Dict[str, Any]: 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({ From 130da25b0b741146ee267764fd12684d248f5548 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 10 Oct 2023 05:32:48 -0500 Subject: [PATCH 86/88] improve slot data documentation --- docs/world api.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/world api.md b/docs/world api.md index a50bef648cd4..c51d50c7ddf0 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -714,6 +714,15 @@ def generate_output(self, output_directory: str): ### 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 in the `fill_slot_data` method on your world. This can be used to fill any +data you need to know client side, and should return a `Dict[Any]`, but should be limited to only data that is +absolutely necessary to not waste data. 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 From d8fab8ee3fdd62079d2b2876d0a88314e3f7de16 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Tue, 10 Oct 2023 12:08:04 -0700 Subject: [PATCH 87/88] remove `CommonOptions.get_value` (#21) --- Options.py | 8 ----- .../stardew_valley/test/checks/goal_checks.py | 6 ++-- .../test/checks/option_checks.py | 36 +++++-------------- 3 files changed, 12 insertions(+), 38 deletions(-) diff --git a/Options.py b/Options.py index da2723d42a67..d9ddfc2e2fdb 100644 --- a/Options.py +++ b/Options.py @@ -955,14 +955,6 @@ def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}") return option_results - def get_value(self, option_name: str) -> typing.Any: - """ - Returns the value of a given option - - :param option_name: names of the option to return the value of - """ - return getattr(self, option_name).value - class LocalItems(ItemSet): """Forces these items to be in their native world.""" diff --git a/worlds/stardew_valley/test/checks/goal_checks.py b/worlds/stardew_valley/test/checks/goal_checks.py index e1059fe2d641..d0f06a6caafa 100644 --- a/worlds/stardew_valley/test/checks/goal_checks.py +++ b/worlds/stardew_valley/test/checks/goal_checks.py @@ -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): diff --git a/worlds/stardew_valley/test/checks/option_checks.py b/worlds/stardew_valley/test/checks/option_checks.py index 06a97b8a256f..ce8e552461e3 100644 --- a/worlds/stardew_valley/test/checks/option_checks.py +++ b/worlds/stardew_valley/test/checks/option_checks.py @@ -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 ...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 - - -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.get_value(setting_name) - return current_value == setting_value - + raise ValueError("no stardew world in this multiworld") -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.get_value(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) - - From 1e707c71be8ba18ceb41acb9104ab973d9dd9b8a Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 10 Oct 2023 14:41:45 -0500 Subject: [PATCH 88/88] better slot data description Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- docs/world api.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/world api.md b/docs/world api.md index c51d50c7ddf0..6fb5b3ac9c6d 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -715,10 +715,10 @@ def generate_output(self, output_directory: str): ### 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 in the `fill_slot_data` method on your world. This can be used to fill any -data you need to know client side, and should return a `Dict[Any]`, but should be limited to only data that is -absolutely necessary to not waste data. 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 +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.