diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 66229600a5..19b1b800ec 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -82,7 +82,7 @@ class WitnessWorld(World): item_name_groups = static_witness_items.ITEM_GROUPS location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS - required_client_version = (0, 6, 4) + required_client_version = (0, 6, 8) player_logic: WitnessPlayerLogic player_locations: WitnessPlayerLocations @@ -110,7 +110,7 @@ class WitnessWorld(World): "hunt_entities": [int(h, 16) for h in self.player_logic.HUNT_ENTITIES], "log_ids_to_hints": self.log_ids_to_hints, "laser_ids_to_hints": self.laser_ids_to_hints, - "progressive_item_lists": self.player_items.get_progressive_item_ids_in_pool(), + "progressive_item_lists": self.player_items.get_progressive_item_ids(), "obelisk_side_id_to_EPs": static_witness_logic.OBELISK_SIDE_ID_TO_EP_HEXES, "precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_ENTITIES], "panel_hunt_required_absolute": self.panel_hunt_required_count @@ -429,19 +429,47 @@ class WitnessWorld(World): else: item_data = static_witness_items.ITEM_DATA[item_name] - return WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player) + item = WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player) - def collect(self, state: "CollectionState", item: WitnessItem) -> bool: - changed = super().collect(state, item) - if changed and item.eggs: + item.is_alias_for = static_witness_items.ALL_ITEM_ALIASES.get(item_name, None) + if hasattr(self, "player_items") and self.player_items: + item.progressive_chain = self.player_items.all_progressive_item_lists.get(item_name, None) + + return item + + def collect(self, state: CollectionState, item: WitnessItem) -> bool: + if not super().collect(state, item): + return False + + if item.eggs: state.prog_items[self.player]["Egg"] += item.eggs - return changed - def remove(self, state: "CollectionState", item: WitnessItem) -> bool: - changed = super().remove(state, item) - if changed and item.eggs: + elif item.is_alias_for: + state.prog_items[self.player][item.is_alias_for] += 1 + + elif item.progressive_chain: + index = state.prog_items[self.player][item.name] - 1 + if index < len(item.progressive_chain): + state.prog_items[self.player][item.progressive_chain[index]] += 1 + + return True + + def remove(self, state: CollectionState, item: WitnessItem) -> bool: + if not super().remove(state, item): + return False + + if item.eggs: state.prog_items[self.player]["Egg"] -= item.eggs - return changed + + elif item.is_alias_for: + state.prog_items[self.player][item.is_alias_for] -= 1 + + elif item.progressive_chain: + index = state.prog_items[self.player][item.name] + if index < len(item.progressive_chain): + state.prog_items[self.player][item.progressive_chain[index]] -= 1 + + return True def get_filler_item_name(self) -> str: return "Speed Boost" diff --git a/worlds/witness/data/WitnessItems.txt b/worlds/witness/data/WitnessItems.txt index 57aee28e45..4ebbc49669 100644 --- a/worlds/witness/data/WitnessItems.txt +++ b/worlds/witness/data/WitnessItems.txt @@ -2,8 +2,8 @@ Symbols: 0 - Dots 1 - Colored Dots 2 - Full Dots -3 - Invisible Dots 5 - Sound Dots +7 - Sparse Dots 10 - Symmetry 20 - Triangles 30 - Eraser @@ -12,12 +12,16 @@ Symbols: 50 - Negative Shapers 60 - Stars 61 - Stars + Same Colored Symbol +67 - Simple Stars 71 - Black/White Squares 72 - Colored Squares 80 - Arrows -200 - Progressive Dots - Dots,Full Dots -210 - Progressive Symmetry - Symmetry,Colored Dots -260 - Progressive Stars - Stars,Stars + Same Colored Symbol +200 - Progressive Dots +240 - Progressive Shapers +210 - Progressive Symmetry +260 - Progressive Stars +270 - Progressive Squares +280 - Progressive Discard Symbols Useful: 510 - Puzzle Skip diff --git a/worlds/witness/data/item_definition_classes.py b/worlds/witness/data/item_definition_classes.py index b095a83abe..3eb77d7509 100644 --- a/worlds/witness/data/item_definition_classes.py +++ b/worlds/witness/data/item_definition_classes.py @@ -35,7 +35,7 @@ class ItemDefinition: @dataclass(frozen=True) class ProgressiveItemDefinition(ItemDefinition): - child_item_names: List[str] + pass @dataclass(frozen=True) diff --git a/worlds/witness/data/settings/Symbol_Shuffle.txt b/worlds/witness/data/settings/Symbol_Shuffle.txt index 253fe98bad..604d1c1064 100644 --- a/worlds/witness/data/settings/Symbol_Shuffle.txt +++ b/worlds/witness/data/settings/Symbol_Shuffle.txt @@ -1,13 +1,18 @@ Items: Arrows -Progressive Dots +Dots +Sparse Dots +Full Dots Sound Dots -Progressive Symmetry +Symmetry +Colored Dots Triangles Eraser Shapers Rotated Shapers Negative Shapers -Progressive Stars +Stars +Simple Stars +Stars + Same Colored Symbol Black/White Squares Colored Squares \ No newline at end of file diff --git a/worlds/witness/data/settings/progressive_items.py b/worlds/witness/data/settings/progressive_items.py new file mode 100644 index 0000000000..869f0b30fc --- /dev/null +++ b/worlds/witness/data/settings/progressive_items.py @@ -0,0 +1,8 @@ +PROGRESSIVE_SYMBOLS = { + "Progressive Dots": ["Dots", "Full Dots"], + "Progressive Symmetry": ["Symmetry", "Colored Dots"], + "Progressive Stars": ["Stars", "Stars + Same Colored Symbol"], + "Progressive Shapers": ["Shapers", "Rotated Shapers", "Negative Shapers"], + "Progressive Squares": ["Black/White Squares", "Colored Squares"], + "Progressive Discard Symbols": ["Triangles", "Arrows"], +} diff --git a/worlds/witness/data/static_items.py b/worlds/witness/data/static_items.py index c64df74198..afd592c3bd 100644 --- a/worlds/witness/data/static_items.py +++ b/worlds/witness/data/static_items.py @@ -13,7 +13,15 @@ ITEM_GROUPS: Dict[str, Set[str]] = {} # item list during get_progression_items. _special_usefuls: List[str] = ["Puzzle Skip"] -ALWAYS_GOOD_SYMBOL_ITEMS: Set[str] = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"} + +ALL_ITEM_ALIASES: Dict[str, str] = { # Keeping this as str->str for now for efficiency + "Sparse Dots": "Dots", + "Simple Stars": "Stars", +} + +ALWAYS_GOOD_SYMBOL_ITEMS: Set[str] = { + "Dots", "Sparse Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars", "Simple Stars" +} MODE_SPECIFIC_GOOD_ITEMS: Dict[str, Set[str]] = { "none": set(), diff --git a/worlds/witness/data/static_logic.py b/worlds/witness/data/static_logic.py index bfe92467fb..4445f56672 100644 --- a/worlds/witness/data/static_logic.py +++ b/worlds/witness/data/static_logic.py @@ -294,7 +294,6 @@ class StaticWitnessLogicObj: # Item data parsed from WitnessItems.txt ALL_ITEMS: Dict[str, ItemDefinition] = {} -_progressive_lookup: Dict[str, str] = {} def parse_items() -> None: @@ -328,22 +327,13 @@ def parse_items() -> None: # Read filler weights. weight = int(arguments[0]) if len(arguments) >= 1 else 1 ALL_ITEMS[item_name] = WeightedItemDefinition(item_code, current_category, weight) - elif arguments: + elif item_name.startswith("Progressive"): # Progressive items. - ALL_ITEMS[item_name] = ProgressiveItemDefinition(item_code, current_category, arguments) - for child_item in arguments: - _progressive_lookup[child_item] = item_name + ALL_ITEMS[item_name] = ProgressiveItemDefinition(item_code, current_category) else: ALL_ITEMS[item_name] = ItemDefinition(item_code, current_category) -def get_parent_progressive_item(item_name: str) -> str: - """ - Returns the name of the item's progressive parent, if there is one, or the item's name if not. - """ - return _progressive_lookup.get(item_name, item_name) - - @cache_argsless def get_vanilla() -> StaticWitnessLogicObj: return StaticWitnessLogicObj(get_vanilla_logic()) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index f04c1f6d37..7f5469a3d5 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -46,6 +46,8 @@ def get_always_hint_items(world: "WitnessWorld") -> List[str]: "Boat", "Caves Shortcuts", "Progressive Dots", + "Dots", + "Sparse Dots", ] difficulty = world.options.puzzle_randomization @@ -100,7 +102,11 @@ def get_priority_hint_items(world: "WitnessWorld") -> List[str]: if world.options.shuffle_symbols: symbols = [ "Progressive Dots", + "Dots", + "Sparse Dots", "Progressive Stars", + "Stars", + "Simple Stars", "Shapers", "Rotated Shapers", "Negative Shapers", @@ -110,9 +116,15 @@ def get_priority_hint_items(world: "WitnessWorld") -> List[str]: "Black/White Squares", "Colored Squares", "Sound Dots", - "Progressive Symmetry" + "Progressive Symmetry", + "Progressive Shapers", + "Progressive Squares", + "Progressive Discard Symbols", ] + # Only consider symbols that are actually in the pool + symbols = [symbol for symbol in symbols if symbol in world.player_items.item_data] + priority.update(world.random.sample(symbols, 5)) if world.options.shuffle_lasers: diff --git a/worlds/witness/options.py b/worlds/witness/options.py index 546f2a5ae2..2fcd35b64c 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -1,8 +1,6 @@ from dataclasses import dataclass from typing import Tuple -from schema import And, Schema - from Options import ( Choice, DefaultOnToggle, @@ -76,6 +74,72 @@ class ShuffleSymbols(DefaultOnToggle): display_name = "Shuffle Symbols" +class ProgressiveSymbols(OptionSet): + """ + Make some symbols progressive, if they exist. + + By default, includes the chains where the second item can't be used without the first. + + Progressive Dots: Dots -> Full Dots + Progressive Symmetry: Symmetry -> Colored Dots + Progressive Stars: Stars -> Stars + Same Colored Symbol + Progressive Squares: Black/White Squares -> Colored Squares + Progressive Shapers: Shapers -> Rotated Shapers -> Negative Shapers + Progressive Discard Symbols: Triangles -> Arrows + """ + display_name = "Progressive Symbols" + + valid_keys = { + "Progressive Dots", + "Progressive Symmetry", + "Progressive Stars", + "Progressive Squares", + "Progressive Shapers", + "Progressive Discard Symbols" + } + + default = frozenset({"Progressive Dots", "Progressive Symmetry", "Progressive Stars"}) + + +class SecondStageSymbolsActIndependently(OptionSet): + """ + Makes certain second stage symbols act independently of first stage symbols if they are not progressive. + + - "Full Dots": "Full Dots" unlocks Full Dots panels even if you don't have "Dots". "Dots" is renamed to "Sparse Dots". + - "Stars + Same Colored Symbol": "Stars + Same Colored Symbol" unlocks Stars + Same Colored Symbol panels even if you don't have "Stars". "Stars" is renamed to "Simlpe Stars". + - "Colored Dots": Removes the Symmetry requirement from the Symmetry Laser panel sets so that Colored Dots can unlock something on their own. This is on by default. + + Rotated Shapers always act independently from Shapers. The ability to make them dependent on Shapers by omitting them in this option may be added in the future. + """ + + valid_keys = { + "Full Dots", + "Stars + Same Colored Symbol", + "Colored Dots", + } + + default = frozenset({"Colored Dots"}) + + visibility = Visibility.template | Visibility.complex_ui + + +class ColoredDotsAreProgressiveDots(Toggle): + """ + Put Colored Dots into the "Progressive Dots" group, after Dots. + This removes Progressive Symmetry. + """ + + visibility = Visibility.template | Visibility.complex_ui + + +class SoundDotsAreProgressiveDots(Toggle): + """ + Put Sound Dots into the "Progressive Dots" group, before Full Dots. + """ + + visibility = Visibility.template | Visibility.complex_ui + + class ShuffleLasers(Choice): """ If on, the 11 lasers are turned into items and will activate on their own upon receiving them. @@ -537,6 +601,10 @@ class PuzzleRandomizationSeed(Range): class TheWitnessOptions(PerGameCommonOptions): puzzle_randomization: PuzzleRandomization shuffle_symbols: ShuffleSymbols + progressive_symbols: ProgressiveSymbols + colored_dots_are_progressive_dots: ColoredDotsAreProgressiveDots + sound_dots_are_progressive_dots: SoundDotsAreProgressiveDots + second_stage_symbols_act_independently: SecondStageSymbolsActIndependently shuffle_doors: ShuffleDoors door_groupings: DoorGroupings shuffle_boat: ShuffleBoat @@ -600,6 +668,10 @@ witness_option_groups = [ ]), OptionGroup("Progression Items", [ ShuffleSymbols, + ProgressiveSymbols, + SecondStageSymbolsActIndependently, + ColoredDotsAreProgressiveDots, + SoundDotsAreProgressiveDots, ShuffleDoors, DoorGroupings, ShuffleLasers, diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 1be2285304..dd2856a727 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Dict, List, Set from BaseClasses import Item, ItemClassification, MultiWorld from .data import static_items as static_witness_items -from .data import static_logic as static_witness_logic from .data.item_definition_classes import ( DoorItemDefinition, ItemCategory, @@ -32,6 +31,8 @@ class WitnessItem(Item): """ game: str = "The Witness" eggs: int = 0 + is_alias_for: str | None = None + progressive_chain: list[str] | None = None @classmethod def make_egg_event(cls, item_name: str, player: int): @@ -55,12 +56,31 @@ class WitnessPlayerItems: self._logic: WitnessPlayerLogic = player_logic self._locations: WitnessPlayerLocations = player_locations + self.replacement_items = {} + # Make item aliases for "Sparse Dots" and "Simple Stars" if necessary + if "Full Dots" in world.options.second_stage_symbols_act_independently: + self.replacement_items["Dots"] = "Sparse Dots" + if "Stars + Same Colored Symbol" in world.options.second_stage_symbols_act_independently: + self.replacement_items["Stars"] = "Simple Stars" + + assert all( + static_witness_items.ALL_ITEM_ALIASES.get(value, None) == key + for key, value in self.replacement_items.items() + ), "A replacement item was used without setting up the alias in static_witness_items.ALL_ITEM_ALIASES" + + self.all_progressive_item_lists = copy.deepcopy(self._logic.THEORETICAL_PROGRESSIVE_LISTS) + self.progressive_item_lists_in_use = copy.deepcopy(self._logic.FINALIZED_PROGRESSIVE_LISTS) + + self.progressive_item_lookup: Dict[str, str] = {} + for progressive_item, chain_items in self.progressive_item_lists_in_use.items(): + self.progressive_item_lookup.update({chain_item: progressive_item for chain_item in chain_items}) + # Duplicate the static item data, then make any player-specific adjustments to classification. self.item_data: Dict[str, ItemData] = copy.deepcopy(static_witness_items.ITEM_DATA) # Remove all progression items that aren't actually in the game. self.item_data = { - name: data for (name, data) in self.item_data.items() + self.replacement_items.get(name, name): data for (name, data) in self.item_data.items() if ItemClassification.progression not in data.classification or name in player_logic.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME } @@ -86,7 +106,7 @@ class WitnessPlayerItems: } for item_name, item_data in progression_dict.items(): if isinstance(item_data.definition, ProgressiveItemDefinition): - num_progression = len(self._logic.PROGRESSIVE_LISTS[item_name]) + num_progression = len(self.progressive_item_lists_in_use[item_name]) self._mandatory_items[item_name] = num_progression else: self._mandatory_items[item_name] = 1 @@ -141,9 +161,15 @@ class WitnessPlayerItems: if self._world.options.puzzle_randomization == "umbra_variety": self._proguseful_items.add("Triangles") - # This needs to be improved when the improved independent&progressive symbols PR is merged - for item in list(self._proguseful_items): - self._proguseful_items.add(static_witness_logic.get_parent_progressive_item(item)) + for progressive_item, progressive_item_chain in player_logic.FINALIZED_PROGRESSIVE_LISTS.items(): + for chain_item in progressive_item_chain: + if chain_item in self._proguseful_items: + self._proguseful_items.add(progressive_item) + break + + for alias_item, real_item in static_witness_items.ALL_ITEM_ALIASES.items(): + if real_item in self._proguseful_items: + self._proguseful_items.add(alias_item) for item_name, item_data in self.item_data.items(): if item_name in self._proguseful_items: @@ -215,7 +241,7 @@ class WitnessPlayerItems: # Replace progressive items with their parents. good_symbols = [ - static_witness_logic.get_parent_progressive_item(item) for item in good_symbols + self.progressive_item_lookup.get(item, item) for item in good_symbols ] output["Symbol"] = [symbol for symbol in good_symbols if symbol in existing_items] @@ -331,13 +357,14 @@ class WitnessPlayerItems: if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL ] - def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]: - output: Dict[int, List[int]] = {} - for item_name, quantity in dict(self._mandatory_items.items()).items(): - item = self.item_data[item_name] - if isinstance(item.definition, ProgressiveItemDefinition): - # Note: we need to reference the static table here rather than the player-specific one because the child - # items were removed from the pool when we pruned out all progression items not in the options. - output[cast_not_none(item.ap_code)] = [cast_not_none(static_witness_items.ITEM_DATA[child_item].ap_code) - for child_item in item.definition.child_item_names] - return output + def get_progressive_item_ids(self) -> Dict[int, List[int]]: + """ + Returns a dict from progressive item IDs to the list of IDs of the base item that they unlock, in order. + """ + return { + cast_not_none(static_witness_items.ITEM_DATA[progressive_item].ap_code): [ + cast_not_none(static_witness_items.ITEM_DATA[base_item].ap_code) + for base_item in corresponding_base_items + ] + for progressive_item, corresponding_base_items in self.all_progressive_item_lists.items() + } diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index b24434732f..c369ec623a 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Dict, List, Set, Tuple, cast from .data import static_logic as static_witness_logic from .data.definition_classes import ConnectionDefinition, WitnessRule from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition +from .data.settings.progressive_items import PROGRESSIVE_SYMBOLS from .data.static_logic import StaticWitnessLogicObj from .data.utils import ( get_boat, @@ -74,12 +75,12 @@ class WitnessPlayerLogic: self.UNREACHABLE_REGIONS: Set[str] = set() self.THEORETICAL_BASE_ITEMS: Set[str] = set() - self.THEORETICAL_ITEMS: Set[str] = set() - self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set() + self.THEORETICAL_PROGRESSIVE_LISTS: Dict[str, List[str]] = {} + self.ENABLED_PROGRESSIVE_LISTS: Dict[str, List[str]] = {} + self.FINALIZED_PROGRESSIVE_LISTS: Dict[str, List[str]] = {} + self.PARENT_ITEM_COUNT_PER_BASE_ITEM: Dict[str, int] = {} + self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set() # No "progressive" conversion yet self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set() - - self.PARENT_ITEM_COUNT_PER_BASE_ITEM: Dict[str, int] = defaultdict(lambda: 1) - self.PROGRESSIVE_LISTS: Dict[str, List[str]] = {} self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {} self.FORBIDDEN_DOORS: Set[str] = set() @@ -303,15 +304,13 @@ class WitnessPlayerLogic: line_split = line.split(" - ") item_name = line_split[0] + # Do not add progressive items, delete the individual items + assert not isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition) + if item_name not in static_witness_items.ITEM_DATA: raise RuntimeError(f'Item "{item_name}" does not exist.') - self.THEORETICAL_ITEMS.add(item_name) - if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition): - self.THEORETICAL_BASE_ITEMS.update(cast(ProgressiveItemDefinition, - static_witness_logic.ALL_ITEMS[item_name]).child_item_names) - else: - self.THEORETICAL_BASE_ITEMS.add(item_name) + self.THEORETICAL_BASE_ITEMS.add(item_name) if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]: entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes @@ -323,13 +322,10 @@ class WitnessPlayerLogic: if adj_type == "Remove Items": item_name = line - self.THEORETICAL_ITEMS.discard(item_name) - if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition): - self.THEORETICAL_BASE_ITEMS.difference_update( - cast(ProgressiveItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).child_item_names - ) - else: - self.THEORETICAL_BASE_ITEMS.discard(item_name) + self.THEORETICAL_BASE_ITEMS.discard(item_name) + + # Do not delete progressive items, delete the individual items + assert not isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition) if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]: entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes @@ -537,6 +533,52 @@ class WitnessPlayerLogic: return postgame_adjustments + def add_implicit_dependencies_to_requirements(self, dependencies: Dict[str, str]) -> None: + if not dependencies: + return + + for entity, requirement in self.DEPENDENT_REQUIREMENTS_BY_HEX.items(): + if "items" not in requirement: + continue + + new_requirement_options = set() + for requirement_option in requirement["items"]: + changed_requirement_option = set(requirement_option) + for item1, item2 in dependencies.items(): + if item1 in requirement_option: + changed_requirement_option.add(item2) + new_requirement_options.add(frozenset(changed_requirement_option)) + self.DEPENDENT_REQUIREMENTS_BY_HEX[entity]["items"] = frozenset(new_requirement_options) + + def adjust_requirements_for_second_stage_symbols(self, world: "WitnessWorld") -> None: + """ + When playing with non-progressive symbols, + there are some second-stage symbols that can't be used without the first-stage symbol. + However, there is a player option that makes these second stage symbols independent. + + If they are independent, we rename "Dots" to "Sparse Dots" and "Stars" to "Simple Stars" + to drive home the separation of the respective items. + + If they are not independent, we need to add the "Dots" requirement to every "Full Dots" panel, + as well as the "Stars" requirement to every "Stars + Same Colored Symbol" panel. + + Also, if Progressive Symmetry is off and independent symbols are off, a Symmetry requirement is added to the + Symmetry Laser sets. + """ + + implicit_dependencies = {} + + if "Full Dots" not in world.options.second_stage_symbols_act_independently: + implicit_dependencies["Full Dots"] = "Dots" + + if "Stars + Same Colored Symbol" not in world.options.second_stage_symbols_act_independently: + implicit_dependencies["Stars + Same Colored Symbol"] = "Stars" + + if "Colored Dots" not in world.options.second_stage_symbols_act_independently: + implicit_dependencies["Colored Dots"] = "Symmetry" + + self.add_implicit_dependencies_to_requirements(implicit_dependencies) + def set_easter_egg_requirements(self, world: "WitnessWorld") -> None: eggs_per_check, logically_required_eggs_per_check = world.options.easter_egg_hunt.get_step_and_logical_step() @@ -649,6 +691,28 @@ class WitnessPlayerLogic: if world.options.shuffle_symbols: adjustment_linesets_in_order.append(get_symbol_shuffle_list()) + self.THEORETICAL_PROGRESSIVE_LISTS = copy.deepcopy(PROGRESSIVE_SYMBOLS) + + self.adjust_requirements_for_second_stage_symbols(world) + + if world.options.colored_dots_are_progressive_dots: + # Insert after Dots + dots_index = self.THEORETICAL_PROGRESSIVE_LISTS["Progressive Dots"].index("Dots") + self.THEORETICAL_PROGRESSIVE_LISTS["Progressive Dots"].insert(dots_index + 1, "Colored Dots") + + # Remove from Progressive Symmetry + self.THEORETICAL_PROGRESSIVE_LISTS["Progressive Symmetry"].remove("Colored Dots") + + if world.options.sound_dots_are_progressive_dots: + # Insert before Full Dots + full_dots_index = self.THEORETICAL_PROGRESSIVE_LISTS["Progressive Dots"].index("Full Dots") + self.THEORETICAL_PROGRESSIVE_LISTS["Progressive Dots"].insert(full_dots_index, "Sound Dots") + + self.ENABLED_PROGRESSIVE_LISTS = { + progressive_item: item_list for progressive_item, item_list in self.THEORETICAL_PROGRESSIVE_LISTS.items() + if progressive_item in world.options.progressive_symbols + } + if world.options.EP_difficulty == "normal": adjustment_linesets_in_order.append(get_ep_easy()) elif world.options.EP_difficulty == "tedious": @@ -962,18 +1026,32 @@ class WitnessPlayerLogic: """ Finalise which items are used in the world, and handle their progressive versions. """ - for item in self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME: - if item not in self.THEORETICAL_ITEMS: - progressive_item_name = static_witness_logic.get_parent_progressive_item(item) - self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name) - child_items = cast(ProgressiveItemDefinition, - static_witness_logic.ALL_ITEMS[progressive_item_name]).child_item_names - progressive_list = [child_item for child_item in child_items - if child_item in self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME] - self.PARENT_ITEM_COUNT_PER_BASE_ITEM[item] = progressive_list.index(item) + 1 - self.PROGRESSIVE_LISTS[progressive_item_name] = progressive_list - else: - self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME.add(item) + + self.FINALIZED_PROGRESSIVE_LISTS = self.ENABLED_PROGRESSIVE_LISTS.copy() + + # Filter non existent base items + self.FINALIZED_PROGRESSIVE_LISTS = { + progressive_item: [ + item for item in base_items if item in self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME + ] + for progressive_item, base_items in self.FINALIZED_PROGRESSIVE_LISTS.items() + } + + # Filter empty chains / chains with only one item (no point in having those) + self.FINALIZED_PROGRESSIVE_LISTS = { + progressive_item: base_items + for progressive_item, base_items in self.FINALIZED_PROGRESSIVE_LISTS.items() + if len(base_items) >= 2 # No point in a single-item progressive chain + } + + # Build PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME with the finalized progressive item replacements in mind + self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME = self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME.copy() + for progressive_item, base_items in self.FINALIZED_PROGRESSIVE_LISTS.items(): + self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item) + self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME -= set(base_items) + + for i, base_item in enumerate(base_items): + self.PARENT_ITEM_COUNT_PER_BASE_ITEM[base_item] = i + 1 def solvability_guaranteed(self, entity_hex: str) -> bool: return not ( diff --git a/worlds/witness/presets.py b/worlds/witness/presets.py index 934f55b685..170df82172 100644 --- a/worlds/witness/presets.py +++ b/worlds/witness/presets.py @@ -123,7 +123,7 @@ witness_option_presets: Dict[str, Dict[str, Any]] = { "trap_percentage": TrapPercentage.default, "puzzle_skip_amount": 15, "trap_weights": TrapWeights.default, - + "hint_amount": HintAmount.default, "area_hint_percentage": AreaHintPercentage.default, "laser_hints": LaserHints.default, diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 545c3e7dd0..5822aadacf 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -203,10 +203,7 @@ def _has_item(item: str, world: "WitnessWorld", if item == "Theater to Tunnels": return lambda state: _can_do_theater_to_tunnels(state, world) - actual_item = static_witness_logic.get_parent_progressive_item(item) - needed_amount = player_logic.PARENT_ITEM_COUNT_PER_BASE_ITEM[item] - - simple_rule: SimpleItemRepresentation = SimpleItemRepresentation(actual_item, needed_amount) + simple_rule: SimpleItemRepresentation = SimpleItemRepresentation(item, 1) return simple_rule @@ -214,6 +211,7 @@ def optimize_requirement_option(requirement_option: List[Union[CollectionRule, S -> List[Union[CollectionRule, SimpleItemRepresentation]]: """ This optimises out a requirement like [("Progressive Dots": 1), ("Progressive Dots": 2)] to only the "2" version. + It is unclear how much this does after the recent rework the progressive items, but there is no reason to remove it. """ direct_items = [rule for rule in requirement_option if isinstance(rule, SimpleItemRepresentation)] @@ -231,12 +229,15 @@ def optimize_requirement_option(requirement_option: List[Union[CollectionRule, S def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleItemRepresentation]], - player: int) -> List[CollectionRule]: + world: "WitnessWorld") -> List[CollectionRule]: """ Converts a list of CollectionRules and SimpleItemRepresentations to just a list of CollectionRules. If the list is ONLY SimpleItemRepresentations, we can just return a CollectionRule based on state.has_all_counts() """ + player_logic = world.player_logic + player = world.player + collection_rules = [rule for rule in requirement if not isinstance(rule, SimpleItemRepresentation)] item_rules = [rule for rule in requirement if isinstance(rule, SimpleItemRepresentation)] @@ -251,7 +252,7 @@ def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleIte # Sort the list by which item you are least likely to have (E.g. last stage of progressive item chains) sorted_item_list = sorted( item_counts.keys(), - key=lambda item_name: item_counts[item_name] if ("Progressive" in item_name) else 1.5, + key=lambda item_name: player_logic.PARENT_ITEM_COUNT_PER_BASE_ITEM.get(item_name, 1.5), reverse=True # 1.5 because you are less likely to have a single stage item than one copy of a 2-stage chain # I did some testing and every part of this genuinely gives a tiiiiny performance boost over not having it! @@ -272,8 +273,6 @@ def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") - """ Converts a WitnessRule into a CollectionRule. """ - player = world.player - if requirements == frozenset({frozenset()}): return None @@ -284,7 +283,7 @@ def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") - optimized_rule_conversion = [optimize_requirement_option(sublist) for sublist in rule_conversion] - fully_converted_rules = [convert_requirement_option(sublist, player) for sublist in optimized_rule_conversion] + fully_converted_rules = [convert_requirement_option(sublist, world) for sublist in optimized_rule_conversion] if len(fully_converted_rules) == 1: if len(fully_converted_rules[0]) == 1: diff --git a/worlds/witness/test/bases.py b/worlds/witness/test/bases.py index c3b427851a..97f3ff9219 100644 --- a/worlds/witness/test/bases.py +++ b/worlds/witness/test/bases.py @@ -122,6 +122,20 @@ class WitnessTestBase(WorldTestBase): ) item_objects.append(removed_item) + def assert_quantities_in_itempool(self, expected_quantities: Mapping[str, int]) -> None: + for item, expected_quantity in expected_quantities.items(): + with self.subTest(f"Verify that there are {expected_quantity} copies of {item} in the itempool."): + found_items = self.get_items_by_name(item) + self.assertEqual(len(found_items), expected_quantity) + + def assert_item_exists_and_is_proguseful(self, item_name: str, proguseful=True): + items = self.get_items_by_name(item_name) + self.assertTrue(items) + if proguseful: + self.assertTrue(all(item.advancement and item.useful for item in items)) + else: + self.assertTrue(all(item.advancement and not item.useful for item in items)) + class WitnessMultiworldTestBase(MultiworldTestBase): options_per_world: List[Dict[str, Any]] diff --git a/worlds/witness/test/test_symbol_shuffle.py b/worlds/witness/test/test_symbol_shuffle.py index 836b0e327f..f40e1b3036 100644 --- a/worlds/witness/test/test_symbol_shuffle.py +++ b/worlds/witness/test/test_symbol_shuffle.py @@ -1,38 +1,394 @@ from ..test.bases import WitnessMultiworldTestBase, WitnessTestBase -class TestSymbols(WitnessTestBase): +class TestProgressiveSymbols(WitnessTestBase): options = { "early_good_items": {}, + "puzzle_randomization": "umbra_variety", + "progressive_symbols": { + "Progressive Dots", + "Progressive Symmetry", + "Progressive Stars", + "Progressive Squares", + "Progressive Shapers", + "Progressive Discard Symbols" + } } def test_progressive_symbols(self) -> None: """ - Test that Dots & Full Dots are correctly replaced by 2x Progressive Dots, + Test that Full Dots are correctly replaced by 2x Progressive Dots, and test that Dots puzzles and Full Dots puzzles require 1 and 2 copies of this item respectively. """ + expected_quantities = { + # Individual items that are replaced by progressive items + "Dots": 0, + "Sparse Dots": 0, + "Full Dots": 0, + "Symmetry": 0, + "Colored Dots": 0, + "Stars": 0, + "Simple Stars": 0, + "Stars + Same Colored Symbol": 0, + "Black/White Squares": 0, + "Colored Squares": 0, + "Shapers": 0, + "Rotated Shapers": 0, + "Negative Shapers": 0, + "Triangles": 0, + "Arrows": 0, + + # Progressive items + "Progressive Dots": 2, + "Progressive Symmetry": 2, + "Progressive Stars": 2, + "Progressive Squares": 2, + "Progressive Shapers": 3, + "Progressive Discard Symbols": 2, + + # Individual items that still exist because they aren't a part of any progressive chain + "Sound Dots": 1, + } + + self.assert_quantities_in_itempool(expected_quantities) + + with self.subTest("Verify that Dots panels need 1 copy of Progressive Dots and Full Dots panel need 2 copies"): + self.collect_all_but("Progressive Dots") + progressive_dots = self.get_items_by_name("Progressive Dots") + self.assertEqual(len(progressive_dots), 2) + + self.assertFalse(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + + self.collect(progressive_dots.pop()) + + self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + + self.collect(progressive_dots.pop()) + + self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + + with self.subTest("Verify proguseful status of progressive & alias items"): + self.assert_item_exists_and_is_proguseful("Progressive Symmetry", proguseful=False) + self.assert_item_exists_and_is_proguseful("Sound Dots", proguseful=False) + + self.assert_item_exists_and_is_proguseful("Progressive Dots") + self.assert_item_exists_and_is_proguseful("Progressive Stars") + self.assert_item_exists_and_is_proguseful("Progressive Squares") + self.assert_item_exists_and_is_proguseful("Progressive Shapers") + self.assert_item_exists_and_is_proguseful("Progressive Discard Symbols") + + +class TestIndependentSecondStageSymbols(WitnessTestBase): + options = { + "early_good_items": {}, + "puzzle_randomization": "umbra_variety", + "progressive_symbols": {}, + "second_stage_symbols_act_independently": { + "Full Dots", + "Stars + Same Colored Symbol", + "Colored Dots", + }, + "shuffle_doors": "doors", + } + + def test_independent_second_stage_symbols(self) -> None: + expected_quantities = { + # Progressive items shouldn't exist + "Progressive Dots": 0, + "Progressive Symmetry": 0, + "Progressive Stars": 0, + "Progressive Squares": 0, + "Progressive Shapers": 0, + "Progressive Discard Symbols": 0, + + # Dots and Stars are replaced by Sparse Dots and Simple Stars + "Dots": 0, + "Stars": 0, + "Sparse Dots": 1, + "Simple Stars": 1, + + # None of the symbols are progressive, so they should all exist + "Full Dots": 1, + "Symmetry": 1, + "Colored Dots": 1, + "Stars + Same Colored Symbol": 1, + "Black/White Squares": 1, + "Colored Squares": 1, + "Shapers": 1, + "Rotated Shapers": 1, + "Negative Shapers": 1, + "Triangles": 1, + "Arrows": 1, + "Sound Dots": 1, + } + + self.assert_quantities_in_itempool(expected_quantities) + + with self.subTest("Verify that Full Dots panels only need Full Dots"): + self.collect_by_name("Black/White Squares") + self.collect_by_name("Triangles") + self.collect_by_name("Outside Tutorial Outpost Exit (Door)") + + self.assertFalse( + self.multiworld.state.can_reach("Outside Tutorial Outpost Exit Panel", "Location", self.player) + ) + self.collect_by_name("Full Dots") + self.assertTrue( + self.multiworld.state.can_reach("Outside Tutorial Outpost Exit Panel", "Location", self.player) + ) + + with self.subTest("Verify that Stars + Same Colored Symbol panels only need Stars + Same Colored Symbol"): + self.collect_by_name("Eraser") + self.collect_by_name("Quarry Entry 1 (Door)") + self.collect_by_name("Quarry Entry 2 (Door)") + + self.assertFalse( + self.multiworld.state.can_reach("Quarry Stoneworks Entry Left Panel", "Location", self.player) + ) + self.collect_by_name("Stars + Same Colored Symbol") + self.assertTrue( + self.multiworld.state.can_reach("Quarry Stoneworks Entry Left Panel", "Location", self.player) + ) + + with self.subTest("Verify that non-symmetry Colored Dots panels only need Colored Dots"): + self.collect_by_name("Symmetry Island Lower (Door)") + self.collect_by_name("Symmetry Island Upper (Door)") + + self.assertFalse( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.collect_by_name("Colored Dots") + self.assertTrue( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + + with self.subTest("Verify proguseful status of progressive & alias items"): + self.assert_item_exists_and_is_proguseful("Full Dots", proguseful=False) + self.assert_item_exists_and_is_proguseful("Stars + Same Colored Symbol", proguseful=False) + self.assert_item_exists_and_is_proguseful("Arrows", proguseful=False) # Variety + + self.assert_item_exists_and_is_proguseful("Sparse Dots") + self.assert_item_exists_and_is_proguseful("Simple Stars") + self.assert_item_exists_and_is_proguseful("Triangles") # Variety + + +class TestDependentSecondStageSymbols(WitnessTestBase): + options = { + "early_good_items": {}, + "puzzle_randomization": "umbra_variety", + "progressive_symbols": {}, + "second_stage_symbols_act_independently": {}, + "shuffle_doors": "doors", + } + + def test_dependent_second_stage_symbols(self) -> None: + expected_quantities = { + # Progressive items shouldn't exist + "Progressive Dots": 0, + "Progressive Symmetry": 0, + "Progressive Stars": 0, + "Progressive Squares": 0, + "Progressive Shapers": 0, + "Progressive Discard Symbols": 0, + + # Dots and Stars are NOT replaced by Sparse Dots and Simple Stars + "Dots": 1, + "Stars": 1, + "Sparse Dots": 0, + "Simple Stars": 0, + + # None of the symbols are progressive, so they should all exist + "Full Dots": 1, + "Symmetry": 1, + "Colored Dots": 1, + "Stars + Same Colored Symbol": 1, + "Black/White Squares": 1, + "Colored Squares": 1, + "Shapers": 1, + "Rotated Shapers": 1, + "Negative Shapers": 1, + "Triangles": 1, + "Arrows": 1, + "Sound Dots": 1, + } + + self.assert_quantities_in_itempool(expected_quantities) + + with self.subTest("Verify that Full Dots panels need Dots as well"): + self.collect_by_name("Black/White Squares") + self.collect_by_name("Triangles") + self.collect_by_name("Outside Tutorial Outpost Exit (Door)") + + self.assertFalse( + self.multiworld.state.can_reach("Outside Tutorial Outpost Exit Panel", "Location", self.player)) + self.collect_by_name("Full Dots") + self.assertFalse( + self.multiworld.state.can_reach("Outside Tutorial Outpost Exit Panel", "Location", self.player)) + self.collect_by_name("Dots") + self.assertTrue( + self.multiworld.state.can_reach("Outside Tutorial Outpost Exit Panel", "Location", self.player) + ) + + with self.subTest("Verify that Stars + Same Colored Symbol panels need Stars as well"): + self.collect_by_name("Eraser") + self.collect_by_name("Quarry Entry 1 (Door)") + self.collect_by_name("Quarry Entry 2 (Door)") + + self.assertFalse( + self.multiworld.state.can_reach("Quarry Stoneworks Entry Left Panel", "Location", self.player) + ) + self.collect_by_name("Stars + Same Colored Symbol") + self.assertFalse( + self.multiworld.state.can_reach("Quarry Stoneworks Entry Left Panel", "Location", self.player) + ) + self.collect_by_name("Stars") + self.assertTrue( + self.multiworld.state.can_reach("Quarry Stoneworks Entry Left Panel", "Location", self.player) + ) + + with self.subTest("Verify that non-symmetry Colored Dots panels need Symmetry as well"): + self.collect_by_name("Symmetry Island Lower (Door)") + self.collect_by_name("Symmetry Island Upper (Door)") + + self.assertFalse( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.collect_by_name("Colored Dots") + self.assertFalse( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.collect_by_name("Symmetry") + self.assertTrue( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + + with self.subTest("Verify proguseful status of progressive & alias items"): + self.assert_item_exists_and_is_proguseful("Full Dots", proguseful=False) + self.assert_item_exists_and_is_proguseful("Stars + Same Colored Symbol", proguseful=False) + self.assert_item_exists_and_is_proguseful("Arrows", proguseful=False) # Variety + + self.assert_item_exists_and_is_proguseful("Dots") + self.assert_item_exists_and_is_proguseful("Stars") + self.assert_item_exists_and_is_proguseful("Triangles") # Variety + + +class TestAlternateProgressiveDots(WitnessTestBase): + options = { + "early_good_items": {}, + "puzzle_randomization": "umbra_variety", + "progressive_symbols": { + "Progressive Dots", + "Progressive Symmetry" + }, + "second_stage_symbols_act_independently": { + "Full Dots", + "Stars + Same Colored Symbol", + "Colored Dots", + }, + "colored_dots_are_progressive_dots": True, + "sound_dots_are_progressive_dots": True, + "shuffle_doors": "doors", + } + + def test_alternate_progressive_dots(self) -> None: + expected_quantities = { + # Progressive Dots chain now has 4 members + "Progressive Dots": 4, + + # Dots items don't exist because Progressive Dots is on. + # For this test, this includes Colored Dots and Sound Dots as well + "Dots": 0, + "Sparse Dots": 0, + "Colored Dots": 0, + "Sound Dots": 0, + "Full Dots": 0, + + # Progressive Symmetry no longer exists, because Colored Dots is part of the Progressive Dots chain instead + "Progressive Symmetry": 0, + + # Other Progressive Symbols don't exist + "Progressive Stars": 0, + "Progressive Squares": 0, + "Progressive Shapers": 0, + "Progressive Discard Symbols": 0, + + # Other standalone items exist, because they are not progressive + "Symmetry": 1, + "Stars + Same Colored Symbol": 1, + "Black/White Squares": 1, + "Colored Squares": 1, + "Shapers": 1, + "Rotated Shapers": 1, + "Negative Shapers": 1, + "Triangles": 1, + "Arrows": 1, + + # This test is set to have independent symbols, so Simple Stars exist instead of Stars + "Stars": 0, + "Simple Stars": 1, + } + + self.assert_quantities_in_itempool(expected_quantities) + + self.collect_all_but(["Progressive Dots", "Symmetry"]) # Skip Symmetry so we can also test a little quirk progressive_dots = self.get_items_by_name("Progressive Dots") - self.assertEqual(len(progressive_dots), 2) + self.assertEqual(len(progressive_dots), 4) - self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) - self.assertFalse( - self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player) - ) + with self.subTest("Test that one copy of Progressive Dots unlocks Dots panels"): + self.assertFalse(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + self.assertFalse( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.assertFalse(self.multiworld.state.can_reach("Jungle Popup Wall 6", "Location", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) - self.collect(progressive_dots.pop()) + self.collect(progressive_dots.pop()) - self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) - self.assertFalse( - self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player) - ) + self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + self.assertFalse( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.assertFalse(self.multiworld.state.can_reach("Jungle Popup Wall 6", "Location", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) - self.collect(progressive_dots.pop()) + with self.subTest("Test that two copies of Progressive Dots unlocks Colored Dots panels"): + self.collect(progressive_dots.pop()) - self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) - self.assertTrue( - self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player) - ) + self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + # Also test here that these "Colored Dots" act independently from Symmetry like they are supposed to + self.assertTrue( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.assertFalse(self.multiworld.state.can_reach("Jungle Popup Wall 6", "Location", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + + with self.subTest("Test that three copies of Progressive Dots unlocks Sound Dots panels"): + self.collect(progressive_dots.pop()) + + self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + self.assertTrue( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.assertTrue(self.multiworld.state.can_reach("Jungle Popup Wall 6", "Location", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + + with self.subTest("Test that four copies of Progressive Dots unlocks Full Dots panels"): + self.collect(progressive_dots.pop()) + + self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + self.assertTrue( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.assertTrue(self.multiworld.state.can_reach("Jungle Popup Wall 6", "Location", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + + with self.subTest("Verify proguseful status of progressive & alias items"): + self.assert_item_exists_and_is_proguseful("Progressive Dots") + self.assert_item_exists_and_is_proguseful("Simple Stars") class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):