mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-18 14:13:32 -07:00
The Witness: More control over progressive symbols (#3961)
* More control over progressive symbols! * Cleanup crew * lol * Make it configurable how second stage items act on their own * only let independent symbols work if they're not progressive * revert defaults * Better description * comment for reviewers * A complicated docstring for a complicated function * More accurate * This probably works more generically * Actually, this would cause other issues anyway, so no need to make this generic yet * :/ * oops * Change the system to use collect/remove override so that plando and start inventory work correctly * Vi stop doing that thing challenge * Make SecondStateSymbolsActIndependently an OptionSet * jank * oop * this is why we make unit tests I guess * More unit tests * More unit tests * Add note about the absence of Rotated Shapers from the independent symbols optionset * More verbose description * More verbose description * slight reword * Ruff ruff :3 I am a good puppy <3 * Remove invis dots * oops * Remove some unused symbols * display name * linecounting -> discard * Make all progressive chains always work * Unfortunately, this optimisation is now unsafe with plando :( * oops * This is now a possible optimisation * optimise optimise optimise * optimise optimise optimise * fix * ruff * oh * fixed frfr * mypy * Clean up the tests a bit * oop * I actually like this better now, so I'm doing it as default * Put stuff on the actual item class for faster collect/remove * giga oops * Make proguseful work * None item left beef * unnecessary change * formatting * add proguseful test to progressive symbols tests * add proguseful test to progressive symbols tests * clean up collect/remove a bit more * giga lmfao * Put stuff in option groups * Add the new symbol items to hint system * bump req client version * fix soft conflict in unit tests * fix more merge errors
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,7 +35,7 @@ class ItemDefinition:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProgressiveItemDefinition(ItemDefinition):
|
||||
child_item_names: List[str]
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@@ -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
|
||||
8
worlds/witness/data/settings/progressive_items.py
Normal file
8
worlds/witness/data/settings/progressive_items.py
Normal file
@@ -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"],
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user