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:
NewSoupVi 2024-09-05 17:10:09 +02:00 committed by GitHub
parent ceec51b9e1
commit b4752cd32d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1314 additions and 20 deletions

View File

@ -204,11 +204,10 @@ class WitnessWorld(World):
]
if early_items:
random_early_item = self.random.choice(early_items)
if (
self.options.puzzle_randomization == "sigma_expert"
or self.options.victory_condition == "panel_hunt"
):
# In Expert and Panel Hunt, only tag the item as early, rather than forcing it onto the gate.
mode = self.options.puzzle_randomization
if mode == "sigma_expert" or mode == "umbra_variety" 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
self.multiworld.local_early_items[self.player][random_early_item] = 1
else:
# Force the item onto the tutorial gate check and remove it from our random pool.
@ -255,7 +254,7 @@ class WitnessWorld(World):
self.get_region(region).add_locations({loc: self.location_name_to_id[loc]})
warning(
f"""Location "{loc}" had to be added to {self.player_name}'s world
f"""Location "{loc}" had to be added to {self.player_name}'s world
due to insufficient sphere 1 size."""
)

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,22 @@ 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"}
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:
for item_name, definition in static_witness_logic.ALL_ITEMS.items():

View File

@ -17,6 +17,7 @@ from .utils import (
get_items,
get_sigma_expert_logic,
get_sigma_normal_logic,
get_umbra_variety_logic,
get_vanilla_logic,
logical_or_witness_rules,
parse_lambda,
@ -292,6 +293,11 @@ def get_sigma_expert() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_sigma_expert_logic())
@cache_argsless
def get_umbra_variety() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_umbra_variety_logic())
def __getattr__(name: str) -> StaticWitnessLogicObj:
if name == "vanilla":
return get_vanilla()
@ -299,6 +305,8 @@ def __getattr__(name: str) -> StaticWitnessLogicObj:
return get_sigma_normal()
if name == "sigma_expert":
return get_sigma_expert()
if name == "umbra_variety":
return get_umbra_variety()
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

View File

@ -215,6 +215,10 @@ def get_sigma_expert_logic() -> List[str]:
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]:
return get_adjustment_file("WitnessLogicVanilla.txt")

View File

@ -53,7 +53,7 @@ def get_always_hint_items(world: "WitnessWorld") -> List[str]:
wincon = world.options.victory_condition
if discards:
if difficulty == "sigma_expert":
if difficulty == "sigma_expert" or difficulty == "umbra_variety":
always.append("Arrows")
else:
always.append("Triangles")

View File

@ -250,10 +250,15 @@ class PanelHuntDiscourageSameAreaFactor(Range):
class PuzzleRandomization(Choice):
"""
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"
option_sigma_normal = 0
option_sigma_expert = 1
option_umbra_variety = 3
option_none = 2

View File

@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Dict, List, Set, cast
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,
@ -155,16 +154,12 @@ class WitnessPlayerItems:
"""
output: Set[str] = set()
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:
if self._world.options.puzzle_randomization == "sigma_expert":
output.add("Arrows")
else:
output.add("Triangles")
# Replace progressive items with their parents.
output = {static_witness_logic.get_parent_progressive_item(item) for item in output}
output = static_witness_items.ALWAYS_GOOD_SYMBOL_ITEMS | static_witness_items.MODE_SPECIFIC_GOOD_ITEMS[mode]
if discards_on:
output |= static_witness_items.MODE_SPECIFIC_GOOD_DISCARD_ITEMS[mode]
# 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

View File

@ -87,12 +87,14 @@ class WitnessPlayerLogic:
self.DIFFICULTY = world.options.puzzle_randomization
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
elif self.DIFFICULTY == "umbra_variety":
self.REFERENCE_LOGIC = static_witness_logic.umbra_variety
elif self.DIFFICULTY == "none":
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.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME

View File

@ -30,6 +30,8 @@ class WitnessPlayerRegions:
self.reference_logic = static_witness_logic.sigma_normal
elif difficulty == "sigma_expert":
self.reference_logic = static_witness_logic.sigma_expert
elif difficulty == "umbra_variety":
self.reference_logic = static_witness_logic.umbra_variety
else:
self.reference_logic = static_witness_logic.vanilla

View File

@ -96,6 +96,39 @@ class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase):
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):
options = {
"shuffle_lasers": True,

View File

@ -54,6 +54,7 @@ class TestMaxEntityShuffle(WitnessTestBase):
class TestPostgameGroupedDoors(WitnessTestBase):
options = {
"puzzle_randomization": "umbra_variety",
"shuffle_postgame": True,
"shuffle_discarded_panels": True,
"shuffle_doors": "doors",

View File

@ -46,6 +46,9 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
{
"puzzle_randomization": "none",
},
{
"puzzle_randomization": "umbra_variety",
}
]
common_options = {
@ -63,12 +66,15 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
self.assertFalse(self.get_items_by_name("Arrows", 1))
self.assertTrue(self.get_items_by_name("Arrows", 2))
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."):
desert_discard = "0x17CE7"
triangles = frozenset({frozenset({"Triangles"})})
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[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[4].player_logic.REQUIREMENTS_BY_HEX[desert_discard], both)