Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The Witness: Panel Hunt Plando #3549

Merged
merged 22 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 40 additions & 6 deletions worlds/witness/entity_hunt.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections import defaultdict
from logging import debug
from logging import debug, warning
from pprint import pformat
from typing import TYPE_CHECKING, Dict, List, Set, Tuple

Expand Down Expand Up @@ -48,6 +48,8 @@ def __init__(self, player_logic: "WitnessPlayerLogic", world: "WitnessWorld",
self.PRE_PICKED_HUNT_ENTITIES = pre_picked_entities.copy()
self.HUNT_ENTITIES: Set[str] = set()

self._add_plandoed_hunt_panels_to_pre_picked()

self.ALL_ELIGIBLE_ENTITIES, self.ELIGIBLE_ENTITIES_PER_AREA = self._get_eligible_panels()

def pick_panel_hunt_panels(self, total_amount: int) -> Set[str]:
Expand All @@ -69,23 +71,51 @@ def pick_panel_hunt_panels(self, total_amount: int) -> Set[str]:

return self.HUNT_ENTITIES

def _entity_is_eligible(self, panel_hex: str) -> bool:
def _entity_is_eligible(self, panel_hex: str, plando: bool = False) -> bool:
"""
Determine whether an entity is eligible for entity hunt based on player options.
"""
panel_obj = static_witness_logic.ENTITIES_BY_HEX[panel_hex]

return (
self.player_logic.solvability_guaranteed(panel_hex)
and not (
if not self.player_logic.solvability_guaranteed(panel_hex) or panel_hex in self.player_logic.EXCLUDED_LOCATIONS:
if plando:
warning(f"Panel {panel_obj['checkName']} is disabled / excluded and thus not eligible for panel hunt.")
return False

return not (
# Due to an edge case, Discards have to be on in disable_non_randomized even if Discard Shuffle is off.
# However, I don't think they should be hunt panels in this case.
self.player_options.disable_non_randomized_puzzles
and not self.player_options.shuffle_discarded_panels
and panel_obj["locationType"] == "Discard"
)
)

def _add_plandoed_hunt_panels_to_pre_picked(self) -> None:
"""
Add panels the player explicitly specified to be included in panel hunt to the pre picked hunt panels.
Output a warning if a panel could not be added for some reason.
"""

# Plandoed hunt panels should be in random order, but deterministic by seed, so we sort, then shuffle
panels_to_plando = sorted(self.player_options.panel_hunt_plando.value)
self.random.shuffle(panels_to_plando)

for location_name in panels_to_plando:
entity_hex = static_witness_logic.ENTITIES_BY_NAME[location_name]["entity_hex"]

if entity_hex in self.PRE_PICKED_HUNT_ENTITIES:
continue

if self._entity_is_eligible(entity_hex, plando=True):
if len(self.PRE_PICKED_HUNT_ENTITIES) == self.player_options.panel_hunt_total:
warning(
f"Panel {location_name} could not be plandoed as a hunt panel for {self.player_name}'s world, "
f"because it would exceed their panel hunt total."
)
continue

self.PRE_PICKED_HUNT_ENTITIES.add(entity_hex)

def _get_eligible_panels(self) -> Tuple[List[str], Dict[str, Set[str]]]:
"""
There are some entities that are not allowed for panel hunt for various technical of gameplay reasons.
Expand Down Expand Up @@ -214,6 +244,10 @@ def _replace_unfair_hunt_entities_with_good_hunt_entities(self) -> None:
if good_entity in self.HUNT_ENTITIES or good_entity not in self.ALL_ELIGIBLE_ENTITIES:
continue

# ... and it's not a forced pick that should stay the same ...
if bad_entitiy in self.PRE_PICKED_HUNT_ENTITIES:
continue

# ... replace the bad entity with the good entity.
self.HUNT_ENTITIES.remove(bad_entitiy)
self.HUNT_ENTITIES.add(good_entity)
Expand Down
25 changes: 24 additions & 1 deletion worlds/witness/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,21 @@

from schema import And, Schema

from Options import Choice, DefaultOnToggle, OptionDict, OptionGroup, PerGameCommonOptions, Range, Toggle, Visibility
from Options import (
Choice,
DefaultOnToggle,
LocationSet,
OptionDict,
OptionGroup,
PerGameCommonOptions,
Range,
Toggle,
Visibility,
)

from .data import static_logic as static_witness_logic
from .data.item_definition_classes import ItemCategory, WeightedItemDefinition
from .entity_hunt import ALL_HUNTABLE_PANELS


class DisableNonRandomizedPuzzles(Toggle):
Expand Down Expand Up @@ -241,6 +252,16 @@ class PanelHuntDiscourageSameAreaFactor(Range):
default = 40


class PanelHuntPlando(LocationSet):
"""
Specify specific hunt panels you want for your panel hunt game.
"""

display_name = "Panel Hunt Plando"

valid_keys = [static_witness_logic.ENTITIES_BY_HEX[panel_hex]["checkName"] for panel_hex in ALL_HUNTABLE_PANELS]


class PuzzleRandomization(Choice):
"""
Puzzles in this randomizer are randomly generated. This option changes the difficulty/types of puzzles.
Expand Down Expand Up @@ -412,6 +433,7 @@ class TheWitnessOptions(PerGameCommonOptions):
panel_hunt_required_percentage: PanelHuntRequiredPercentage
panel_hunt_postgame: PanelHuntPostgame
panel_hunt_discourage_same_area_factor: PanelHuntDiscourageSameAreaFactor
panel_hunt_plando: PanelHuntPlando
early_caves: EarlyCaves
early_symbol_item: EarlySymbolItem
elevators_come_to_you: ElevatorsComeToYou
Expand All @@ -438,6 +460,7 @@ class TheWitnessOptions(PerGameCommonOptions):
PanelHuntTotal,
PanelHuntPostgame,
PanelHuntDiscourageSameAreaFactor,
PanelHuntPlando,
], start_collapsed=True),
OptionGroup("Locations", [
ShuffleDiscardedPanels,
Expand Down
27 changes: 14 additions & 13 deletions worlds/witness/test/test_panel_hunt.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from BaseClasses import CollectionState, Item
from worlds.witness.test import WitnessTestBase, WitnessMultiworldTestBase
from BaseClasses import CollectionState

from worlds.witness.test import WitnessMultiworldTestBase, WitnessTestBase


class TestMaxPanelHuntMinChecks(WitnessTestBase):
Expand All @@ -13,7 +14,7 @@ class TestMaxPanelHuntMinChecks(WitnessTestBase):
"shuffle_vault_boxes": False,
}

def test_correct_panels_were_picked(self):
def test_correct_panels_were_picked(self) -> None:
with self.subTest("Check that 100 Hunt Panels were actually picked."):
self.assertEqual(len(self.multiworld.find_item_locations("+1 Panel Hunt", self.player)), 100)

Expand Down Expand Up @@ -63,45 +64,45 @@ class TestPanelHuntPostgame(WitnessMultiworldTestBase):
"shuffle_discarded_panels": True,
}

def test_panel_hunt_postgame(self):
def test_panel_hunt_postgame(self) -> None:
for player_minus_one, options in enumerate(self.options_per_world):
player = player_minus_one + 1
postgame_option = options["panel_hunt_postgame"]
with self.subTest(f"Test that \"{postgame_option}\" results in 40 Hunt Panels."):
with self.subTest(f'Test that "{postgame_option}" results in 40 Hunt Panels.'):
self.assertEqual(len(self.multiworld.find_item_locations("+1 Panel Hunt", player)), 40)

# Test that the box gets extra checks from panel_hunt_postgame

with self.subTest("Test that \"everything_is_eligible\" has no Mountaintop Box Hunt Panels."):
with self.subTest('Test that "everything_is_eligible" has no Mountaintop Box Hunt Panels.'):
self.assert_location_does_not_exist("Mountaintop Box Short (Panel Hunt)", 1, strict_check=False)
self.assert_location_does_not_exist("Mountaintop Box Long (Panel Hunt)", 1, strict_check=False)

with self.subTest("Test that \"disable_mountain_lasers_locations\" has a Hunt Panel for Short, but not Long."):
with self.subTest('Test that "disable_mountain_lasers_locations" has a Hunt Panel for Short, but not Long.'):
self.assert_location_exists("Mountaintop Box Short (Panel Hunt)", 2, strict_check=False)
self.assert_location_does_not_exist("Mountaintop Box Long (Panel Hunt)", 2, strict_check=False)

with self.subTest("Test that \"disable_challenge_lasers_locations\" has a Hunt Panel for Long, but not Short."):
with self.subTest('Test that "disable_challenge_lasers_locations" has a Hunt Panel for Long, but not Short.'):
self.assert_location_does_not_exist("Mountaintop Box Short (Panel Hunt)", 3, strict_check=False)
self.assert_location_exists("Mountaintop Box Long (Panel Hunt)", 3, strict_check=False)

with self.subTest("Test that \"disable_anything_locked_by_lasers\" has both Mountaintop Box Hunt Panels."):
with self.subTest('Test that "disable_anything_locked_by_lasers" has both Mountaintop Box Hunt Panels.'):
self.assert_location_exists("Mountaintop Box Short (Panel Hunt)", 4, strict_check=False)
self.assert_location_exists("Mountaintop Box Long (Panel Hunt)", 4, strict_check=False)

# Check panel_hunt_postgame locations get disabled

with self.subTest("Test that \"everything_is_eligible\" does not disable any locked-by-lasers panels."):
with self.subTest('Test that "everything_is_eligible" does not disable any locked-by-lasers panels.'):
self.assert_location_exists("Mountain Floor 1 Right Row 5", 1)
self.assert_location_exists("Mountain Bottom Floor Discard", 1)

with self.subTest("Test that \"disable_mountain_lasers_locations\" disables only Shortbox-Locked panels."):
with self.subTest('Test that "disable_mountain_lasers_locations" disables only Shortbox-Locked panels.'):
self.assert_location_does_not_exist("Mountain Floor 1 Right Row 5", 2)
self.assert_location_exists("Mountain Bottom Floor Discard", 2)

with self.subTest("Test that \"disable_challenge_lasers_locations\" disables only Longbox-Locked panels."):
with self.subTest('Test that "disable_challenge_lasers_locations" disables only Longbox-Locked panels.'):
self.assert_location_exists("Mountain Floor 1 Right Row 5", 3)
self.assert_location_does_not_exist("Mountain Bottom Floor Discard", 3)

with self.subTest("Test that \"everything_is_eligible\" disables only Shortbox-Locked panels."):
with self.subTest('Test that "everything_is_eligible" disables only Shortbox-Locked panels.'):
self.assert_location_does_not_exist("Mountain Floor 1 Right Row 5", 4)
self.assert_location_does_not_exist("Mountain Bottom Floor Discard", 4)
Loading