The Witness: Implement "Variety" puzzles mode (#3239)
* Variety Rando (But WitnessLogicVariety.txt is wrong * Actually variety the variety file (Ty Exempt-Medic <3) * This will be preopened * Tooltip explaining the different difficulties * Remove ?, those were correct * Less efficient but easier to follow * Parentheses * Fix some reqs * Not Arrows in Variety * Oops * Happy medic, I made a wacky solution * there we go * Lint oops * There * that copy is unnecessary * Turns out that copy is necessary still * yes * lol * Rename to Umbra Variety * missed one * Erase the Eraser * Fix remaining instances of 'variety' and don't have a symbol item on the gate in variety * reorder difficulties * inbetween * ruff * Fix Variety Invis requirements * Fix wooden beams variety * Fix PP2 variety * Mirror changes from 'Variety Mode Puzzle Change 3.2.3' * These also have Symmetry * merge error prevention * Update worlds/witness/data/static_items.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * no elif after return * add variety to the symbol requirement bleed test * Add variety to one of the 'other settings' unit tests * Add Variety minimal symbols unittest * oops * I did the dumb again * . * Incorporate changes from other PR into WitnesLogicVariety.txt * Update worlds/witness/data/WitnessLogicVariety.txt Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/witness/data/WitnessLogicVariety.txt Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update the reqs as well haha * Another difference, thanks Medic :§ * Wait no, this one was right * lol * apply changes to WitnessLogicVariety.txt * Add most recent Variety changes * oof --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
parent
ceec51b9e1
commit
b4752cd32d
|
@ -204,11 +204,10 @@ class WitnessWorld(World):
|
||||||
]
|
]
|
||||||
if early_items:
|
if early_items:
|
||||||
random_early_item = self.random.choice(early_items)
|
random_early_item = self.random.choice(early_items)
|
||||||
if (
|
mode = self.options.puzzle_randomization
|
||||||
self.options.puzzle_randomization == "sigma_expert"
|
if mode == "sigma_expert" or mode == "umbra_variety" or self.options.victory_condition == "panel_hunt":
|
||||||
or self.options.victory_condition == "panel_hunt"
|
# In Expert and Variety, only tag the item as early, rather than forcing it onto the gate.
|
||||||
):
|
# Same with panel hunt, since the Tutorial Gate Open panel is used for something else
|
||||||
# In Expert and Panel Hunt, only tag the item as early, rather than forcing it onto the gate.
|
|
||||||
self.multiworld.local_early_items[self.player][random_early_item] = 1
|
self.multiworld.local_early_items[self.player][random_early_item] = 1
|
||||||
else:
|
else:
|
||||||
# Force the item onto the tutorial gate check and remove it from our random pool.
|
# Force the item onto the tutorial gate check and remove it from our random pool.
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -13,6 +13,22 @@ ITEM_GROUPS: Dict[str, Set[str]] = {}
|
||||||
# item list during get_progression_items.
|
# item list during get_progression_items.
|
||||||
_special_usefuls: List[str] = ["Puzzle Skip"]
|
_special_usefuls: List[str] = ["Puzzle Skip"]
|
||||||
|
|
||||||
|
ALWAYS_GOOD_SYMBOL_ITEMS: Set[str] = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
|
||||||
|
|
||||||
|
MODE_SPECIFIC_GOOD_ITEMS: Dict[str, Set[str]] = {
|
||||||
|
"none": set(),
|
||||||
|
"sigma_normal": set(),
|
||||||
|
"sigma_expert": {"Triangles"},
|
||||||
|
"umbra_variety": {"Triangles"}
|
||||||
|
}
|
||||||
|
|
||||||
|
MODE_SPECIFIC_GOOD_DISCARD_ITEMS: Dict[str, Set[str]] = {
|
||||||
|
"none": {"Triangles"},
|
||||||
|
"sigma_normal": {"Triangles"},
|
||||||
|
"sigma_expert": {"Arrows"},
|
||||||
|
"umbra_variety": set() # Variety Discards use both Arrows and Triangles, so neither of them are that useful alone
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def populate_items() -> None:
|
def populate_items() -> None:
|
||||||
for item_name, definition in static_witness_logic.ALL_ITEMS.items():
|
for item_name, definition in static_witness_logic.ALL_ITEMS.items():
|
||||||
|
|
|
@ -17,6 +17,7 @@ from .utils import (
|
||||||
get_items,
|
get_items,
|
||||||
get_sigma_expert_logic,
|
get_sigma_expert_logic,
|
||||||
get_sigma_normal_logic,
|
get_sigma_normal_logic,
|
||||||
|
get_umbra_variety_logic,
|
||||||
get_vanilla_logic,
|
get_vanilla_logic,
|
||||||
logical_or_witness_rules,
|
logical_or_witness_rules,
|
||||||
parse_lambda,
|
parse_lambda,
|
||||||
|
@ -292,6 +293,11 @@ def get_sigma_expert() -> StaticWitnessLogicObj:
|
||||||
return StaticWitnessLogicObj(get_sigma_expert_logic())
|
return StaticWitnessLogicObj(get_sigma_expert_logic())
|
||||||
|
|
||||||
|
|
||||||
|
@cache_argsless
|
||||||
|
def get_umbra_variety() -> StaticWitnessLogicObj:
|
||||||
|
return StaticWitnessLogicObj(get_umbra_variety_logic())
|
||||||
|
|
||||||
|
|
||||||
def __getattr__(name: str) -> StaticWitnessLogicObj:
|
def __getattr__(name: str) -> StaticWitnessLogicObj:
|
||||||
if name == "vanilla":
|
if name == "vanilla":
|
||||||
return get_vanilla()
|
return get_vanilla()
|
||||||
|
@ -299,6 +305,8 @@ def __getattr__(name: str) -> StaticWitnessLogicObj:
|
||||||
return get_sigma_normal()
|
return get_sigma_normal()
|
||||||
if name == "sigma_expert":
|
if name == "sigma_expert":
|
||||||
return get_sigma_expert()
|
return get_sigma_expert()
|
||||||
|
if name == "umbra_variety":
|
||||||
|
return get_umbra_variety()
|
||||||
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -215,6 +215,10 @@ def get_sigma_expert_logic() -> List[str]:
|
||||||
return get_adjustment_file("WitnessLogicExpert.txt")
|
return get_adjustment_file("WitnessLogicExpert.txt")
|
||||||
|
|
||||||
|
|
||||||
|
def get_umbra_variety_logic() -> List[str]:
|
||||||
|
return get_adjustment_file("WitnessLogicVariety.txt")
|
||||||
|
|
||||||
|
|
||||||
def get_vanilla_logic() -> List[str]:
|
def get_vanilla_logic() -> List[str]:
|
||||||
return get_adjustment_file("WitnessLogicVanilla.txt")
|
return get_adjustment_file("WitnessLogicVanilla.txt")
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ def get_always_hint_items(world: "WitnessWorld") -> List[str]:
|
||||||
wincon = world.options.victory_condition
|
wincon = world.options.victory_condition
|
||||||
|
|
||||||
if discards:
|
if discards:
|
||||||
if difficulty == "sigma_expert":
|
if difficulty == "sigma_expert" or difficulty == "umbra_variety":
|
||||||
always.append("Arrows")
|
always.append("Arrows")
|
||||||
else:
|
else:
|
||||||
always.append("Triangles")
|
always.append("Triangles")
|
||||||
|
|
|
@ -250,10 +250,15 @@ class PanelHuntDiscourageSameAreaFactor(Range):
|
||||||
class PuzzleRandomization(Choice):
|
class PuzzleRandomization(Choice):
|
||||||
"""
|
"""
|
||||||
Puzzles in this randomizer are randomly generated. This option changes the difficulty/types of puzzles.
|
Puzzles in this randomizer are randomly generated. This option changes the difficulty/types of puzzles.
|
||||||
|
"Sigma Normal" randomizes puzzles close to their original mechanics and difficulty.
|
||||||
|
"Sigma Expert" is an entirely new experience with extremely difficult random puzzles. Do not underestimate this mode, it is brutal.
|
||||||
|
"Umbra Variety" focuses on unique symbol combinations not featured in the original game. It is harder than Sigma Normal, but easier than Sigma Expert.
|
||||||
|
"None" means that the puzzles are unchanged from the original game.
|
||||||
"""
|
"""
|
||||||
display_name = "Puzzle Randomization"
|
display_name = "Puzzle Randomization"
|
||||||
option_sigma_normal = 0
|
option_sigma_normal = 0
|
||||||
option_sigma_expert = 1
|
option_sigma_expert = 1
|
||||||
|
option_umbra_variety = 3
|
||||||
option_none = 2
|
option_none = 2
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Dict, List, Set, cast
|
||||||
from BaseClasses import Item, ItemClassification, MultiWorld
|
from BaseClasses import Item, ItemClassification, MultiWorld
|
||||||
|
|
||||||
from .data import static_items as static_witness_items
|
from .data import static_items as static_witness_items
|
||||||
from .data import static_logic as static_witness_logic
|
|
||||||
from .data.item_definition_classes import (
|
from .data.item_definition_classes import (
|
||||||
DoorItemDefinition,
|
DoorItemDefinition,
|
||||||
ItemCategory,
|
ItemCategory,
|
||||||
|
@ -155,16 +154,12 @@ class WitnessPlayerItems:
|
||||||
"""
|
"""
|
||||||
output: Set[str] = set()
|
output: Set[str] = set()
|
||||||
if self._world.options.shuffle_symbols:
|
if self._world.options.shuffle_symbols:
|
||||||
output = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
|
discards_on = self._world.options.shuffle_discarded_panels
|
||||||
|
mode = self._world.options.puzzle_randomization.current_key
|
||||||
|
|
||||||
if self._world.options.shuffle_discarded_panels:
|
output = static_witness_items.ALWAYS_GOOD_SYMBOL_ITEMS | static_witness_items.MODE_SPECIFIC_GOOD_ITEMS[mode]
|
||||||
if self._world.options.puzzle_randomization == "sigma_expert":
|
if discards_on:
|
||||||
output.add("Arrows")
|
output |= static_witness_items.MODE_SPECIFIC_GOOD_DISCARD_ITEMS[mode]
|
||||||
else:
|
|
||||||
output.add("Triangles")
|
|
||||||
|
|
||||||
# Replace progressive items with their parents.
|
|
||||||
output = {static_witness_logic.get_parent_progressive_item(item) for item in output}
|
|
||||||
|
|
||||||
# Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved
|
# Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved
|
||||||
# before create_items so that we'll be able to check placed items instead of just removing all items mentioned
|
# before create_items so that we'll be able to check placed items instead of just removing all items mentioned
|
||||||
|
|
|
@ -87,12 +87,14 @@ class WitnessPlayerLogic:
|
||||||
self.DIFFICULTY = world.options.puzzle_randomization
|
self.DIFFICULTY = world.options.puzzle_randomization
|
||||||
|
|
||||||
self.REFERENCE_LOGIC: StaticWitnessLogicObj
|
self.REFERENCE_LOGIC: StaticWitnessLogicObj
|
||||||
if self.DIFFICULTY == "sigma_expert":
|
if self.DIFFICULTY == "sigma_normal":
|
||||||
|
self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
|
||||||
|
elif self.DIFFICULTY == "sigma_expert":
|
||||||
self.REFERENCE_LOGIC = static_witness_logic.sigma_expert
|
self.REFERENCE_LOGIC = static_witness_logic.sigma_expert
|
||||||
|
elif self.DIFFICULTY == "umbra_variety":
|
||||||
|
self.REFERENCE_LOGIC = static_witness_logic.umbra_variety
|
||||||
elif self.DIFFICULTY == "none":
|
elif self.DIFFICULTY == "none":
|
||||||
self.REFERENCE_LOGIC = static_witness_logic.vanilla
|
self.REFERENCE_LOGIC = static_witness_logic.vanilla
|
||||||
else:
|
|
||||||
self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
|
|
||||||
|
|
||||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
|
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
|
||||||
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
|
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
|
||||||
|
|
|
@ -30,6 +30,8 @@ class WitnessPlayerRegions:
|
||||||
self.reference_logic = static_witness_logic.sigma_normal
|
self.reference_logic = static_witness_logic.sigma_normal
|
||||||
elif difficulty == "sigma_expert":
|
elif difficulty == "sigma_expert":
|
||||||
self.reference_logic = static_witness_logic.sigma_expert
|
self.reference_logic = static_witness_logic.sigma_expert
|
||||||
|
elif difficulty == "umbra_variety":
|
||||||
|
self.reference_logic = static_witness_logic.umbra_variety
|
||||||
else:
|
else:
|
||||||
self.reference_logic = static_witness_logic.vanilla
|
self.reference_logic = static_witness_logic.vanilla
|
||||||
|
|
||||||
|
|
|
@ -96,6 +96,39 @@ class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase):
|
||||||
self.assert_can_beat_with_minimally(exact_requirement)
|
self.assert_can_beat_with_minimally(exact_requirement)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSymbolsRequiredToWinElevatorVariety(WitnessTestBase):
|
||||||
|
options = {
|
||||||
|
"shuffle_lasers": True,
|
||||||
|
"mountain_lasers": 1,
|
||||||
|
"victory_condition": "elevator",
|
||||||
|
"early_symbol_item": False,
|
||||||
|
"puzzle_randomization": "umbra_variety",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_symbols_to_win(self) -> None:
|
||||||
|
"""
|
||||||
|
In symbol shuffle, the only way to reach the Elevator is through Mountain Entry by descending the Mountain.
|
||||||
|
This requires a very specific set of symbol items per puzzle randomization mode.
|
||||||
|
In this case, we check Variety Puzzles.
|
||||||
|
"""
|
||||||
|
|
||||||
|
exact_requirement = {
|
||||||
|
"Monastery Laser": 1,
|
||||||
|
"Progressive Dots": 2,
|
||||||
|
"Progressive Stars": 2,
|
||||||
|
"Progressive Symmetry": 1,
|
||||||
|
"Black/White Squares": 1,
|
||||||
|
"Colored Squares": 1,
|
||||||
|
"Shapers": 1,
|
||||||
|
"Rotated Shapers": 1,
|
||||||
|
"Eraser": 1,
|
||||||
|
"Triangles": 1,
|
||||||
|
"Arrows": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assert_can_beat_with_minimally(exact_requirement)
|
||||||
|
|
||||||
|
|
||||||
class TestPanelsRequiredToWinElevator(WitnessTestBase):
|
class TestPanelsRequiredToWinElevator(WitnessTestBase):
|
||||||
options = {
|
options = {
|
||||||
"shuffle_lasers": True,
|
"shuffle_lasers": True,
|
||||||
|
|
|
@ -54,6 +54,7 @@ class TestMaxEntityShuffle(WitnessTestBase):
|
||||||
|
|
||||||
class TestPostgameGroupedDoors(WitnessTestBase):
|
class TestPostgameGroupedDoors(WitnessTestBase):
|
||||||
options = {
|
options = {
|
||||||
|
"puzzle_randomization": "umbra_variety",
|
||||||
"shuffle_postgame": True,
|
"shuffle_postgame": True,
|
||||||
"shuffle_discarded_panels": True,
|
"shuffle_discarded_panels": True,
|
||||||
"shuffle_doors": "doors",
|
"shuffle_doors": "doors",
|
||||||
|
|
|
@ -46,6 +46,9 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
|
||||||
{
|
{
|
||||||
"puzzle_randomization": "none",
|
"puzzle_randomization": "none",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"puzzle_randomization": "umbra_variety",
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
common_options = {
|
common_options = {
|
||||||
|
@ -63,12 +66,15 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
|
||||||
self.assertFalse(self.get_items_by_name("Arrows", 1))
|
self.assertFalse(self.get_items_by_name("Arrows", 1))
|
||||||
self.assertTrue(self.get_items_by_name("Arrows", 2))
|
self.assertTrue(self.get_items_by_name("Arrows", 2))
|
||||||
self.assertFalse(self.get_items_by_name("Arrows", 3))
|
self.assertFalse(self.get_items_by_name("Arrows", 3))
|
||||||
|
self.assertTrue(self.get_items_by_name("Arrows", 4))
|
||||||
|
|
||||||
with self.subTest("Test that Discards ask for Triangles in normal, but Arrows in expert."):
|
with self.subTest("Test that Discards ask for Triangles in normal, but Arrows in expert."):
|
||||||
desert_discard = "0x17CE7"
|
desert_discard = "0x17CE7"
|
||||||
triangles = frozenset({frozenset({"Triangles"})})
|
triangles = frozenset({frozenset({"Triangles"})})
|
||||||
arrows = frozenset({frozenset({"Arrows"})})
|
arrows = frozenset({frozenset({"Arrows"})})
|
||||||
|
both = frozenset({frozenset({"Triangles", "Arrows"})})
|
||||||
|
|
||||||
self.assertEqual(self.multiworld.worlds[1].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
|
self.assertEqual(self.multiworld.worlds[1].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
|
||||||
self.assertEqual(self.multiworld.worlds[2].player_logic.REQUIREMENTS_BY_HEX[desert_discard], arrows)
|
self.assertEqual(self.multiworld.worlds[2].player_logic.REQUIREMENTS_BY_HEX[desert_discard], arrows)
|
||||||
self.assertEqual(self.multiworld.worlds[3].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
|
self.assertEqual(self.multiworld.worlds[3].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
|
||||||
|
self.assertEqual(self.multiworld.worlds[4].player_logic.REQUIREMENTS_BY_HEX[desert_discard], both)
|
||||||
|
|
Loading…
Reference in New Issue