Skip to content

Commit

Permalink
The Witness: Add "vague" hints making use of other games' region name…
Browse files Browse the repository at this point in the history
…s and location groups (#2921)

* Vague hints work! But, the client will probably reveal some of the info through scouts atm

* Fall back on Everywhere if necessary

* Some of these failsafes are not necessary now

* Limit region size to 100 as well

* Actually... like this.

* Nutmeg

* Lol

* -1 for own player but don't scout

* Still make always/priority ITEM hints

* fix

* uwu notices your bug

* The hints should, like, actually work, you know?

* Make it a Toggle

* Update worlds/witness/hints.py

Co-authored-by: Bryce Wilson <[email protected]>

* Update worlds/witness/hints.py

Co-authored-by: Bryce Wilson <[email protected]>

* Make some suggested changes

* Make that ungodly equation a bit clearer in terms of formatting

* make that not sorted

* Add a warning about the feature in the option tooltip

* Make using region names experimental

* reword option tooltip

* Note about singleplayer

* Slight rewording again

* Reorder the order of priority a bit

* this condition is unnecessary now

* comment

* No wait the order has to be like this

* Okay now I think it's correct

* Another comment

* Align option tooltip with new behavior

* slight rewording again

* reword reword reword reword

* -

* ethics

* Update worlds/witness/options.py

Co-authored-by: Bryce Wilson <[email protected]>

* Rename and slight behavior change for local hints

* I think I overengineered this system before. Make it more consistent and clear now

* oops I used checks by accident

* oops

* OMEGA OOPS

* Accidentally commited a print statemetn

* Vi don't commit nonsense challenge difficulty impossible

* This isn't always true but it's good enough

* Update options.py

* Update worlds/witness/options.py

Co-authored-by: Scipio Wright <[email protected]>

* Scipio :3

* switch to is_event instead of checking against location.address

* oop

* Update test_roll_other_options.py

* Fix that unit test problem lol

* Oh is this not fixed in the apworld?

---------

Co-authored-by: Bryce Wilson <[email protected]>
Co-authored-by: Scipio Wright <[email protected]>
  • Loading branch information
3 people authored Aug 19, 2024
1 parent f253dff commit c4e7b6c
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 36 deletions.
152 changes: 118 additions & 34 deletions worlds/witness/hints.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import logging
import math
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union, cast

from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region

from .data import static_logic as static_witness_logic
from .data.utils import weighted_sample
Expand All @@ -11,8 +12,8 @@
if TYPE_CHECKING:
from . import WitnessWorld

CompactHintArgs = Tuple[Union[str, int], int]
CompactHintData = Tuple[str, Union[str, int], int]
CompactHintArgs = Tuple[Union[str, int], Union[str, int]]
CompactHintData = Tuple[str, Union[str, int], Union[str, int]]


@dataclass
Expand All @@ -37,6 +38,7 @@ class WitnessWordedHint:
area: Optional[str] = None
area_amount: Optional[int] = None
area_hunt_panels: Optional[int] = None
vague_location_hint: bool = False


def get_always_hint_items(world: "WitnessWorld") -> List[str]:
Expand Down Expand Up @@ -170,6 +172,51 @@ def get_priority_hint_locations(world: "WitnessWorld") -> List[str]:
return priority


def try_getting_location_group_for_location(world: "WitnessWorld", hint_loc: Location) -> Tuple[str, str]:
allow_regions = world.options.vague_hints == "experimental"

possible_location_groups = {
group_name: group_locations
for group_name, group_locations in world.multiworld.worlds[hint_loc.player].location_name_groups.items()
if hint_loc.name in group_locations
}

locations_in_that_world = {
location.name for location in world.multiworld.get_locations(hint_loc.player) if not location.is_event
}

valid_location_groups: Dict[str, int] = {}

# Find valid location groups.
for group, locations in possible_location_groups.items():
if group == "Everywhere":
continue
present_locations = sum(location in locations_in_that_world for location in locations)
valid_location_groups[group] = present_locations

# If there are valid location groups, use a random one.
if valid_location_groups:
# If there are location groups with more than 1 location, remove any that only have 1.
if any(num_locs > 1 for num_locs in valid_location_groups.values()):
valid_location_groups = {name: num_locs for name, num_locs in valid_location_groups.items() if num_locs > 1}

location_groups_with_weights = {
# Listen. Just don't worry about it. :)))
location_group: (x ** 0.6) * math.e ** (- (x / 7) ** 0.6) if x > 6 else x / 6
for location_group, x in valid_location_groups.items()
}

location_groups = list(location_groups_with_weights.keys())
weights = list(location_groups_with_weights.values())

return world.random.choices(location_groups, weights, k=1)[0], "Group"

if allow_regions:
return cast(Region, hint_loc.parent_region).name, "Region"

return "Everywhere", "Everywhere"


def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint:
location_name = hint.location.name
if hint.location.player != world.player:
Expand All @@ -184,12 +231,37 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes
if item.player != world.player:
item_name += " (" + world.multiworld.get_player_name(item.player) + ")"

if hint.hint_came_from_location:
hint_text = f"{location_name} contains {item_name}."
else:
hint_text = f"{item_name} can be found at {location_name}."
hint_text = ""
area: Optional[str] = None

if world.options.vague_hints:
chosen_group, group_type = try_getting_location_group_for_location(world, hint.location)

return WitnessWordedHint(hint_text, hint.location)
if hint.location.player == world.player:
area = chosen_group

# local locations should only ever return a location group, as Witness defines groups for every location.
hint_text = f"{item_name} can be found in the {area} area."
else:
player_name = world.multiworld.get_player_name(hint.location.player)

if group_type == "Everywhere":
location_name = f"a location in {player_name}'s world"
elif group_type == "Group":
location_name = f"a \"{chosen_group}\" location in {player_name}'s world"
elif group_type == "Region":
if chosen_group == "Menu":
location_name = f"a location near the start of {player_name}'s game (\"Menu\" region)"
else:
location_name = f"a location in {player_name}'s \"{chosen_group}\" region"

if hint_text == "":
if hint.hint_came_from_location:
hint_text = f"{location_name} contains {item_name}."
else:
hint_text = f"{item_name} can be found at {location_name}."

return WitnessWordedHint(hint_text, hint.location, area=area, vague_location_hint=bool(world.options.vague_hints))


def hint_from_item(world: "WitnessWorld", item_name: str,
Expand Down Expand Up @@ -224,45 +296,55 @@ def hint_from_location(world: "WitnessWorld", location: str) -> Optional[Witness
return WitnessLocationHint(world.get_location(location), True)


def get_items_and_locations_in_random_order(world: "WitnessWorld",
own_itempool: List["WitnessItem"]) -> Tuple[List[str], List[str]]:
prog_items_in_this_world = sorted(
def get_item_and_location_names_in_random_order(world: "WitnessWorld",
own_itempool: List["WitnessItem"]) -> Tuple[List[str], List[str]]:
prog_item_names_in_this_world = [
item.name for item in own_itempool
if item.advancement and item.code and item.location
)
locations_in_this_world = sorted(
location.name for location in world.multiworld.get_locations(world.player)
if location.address and location.progress_type != LocationProgressType.EXCLUDED
)
]
world.random.shuffle(prog_item_names_in_this_world)

world.random.shuffle(prog_items_in_this_world)
locations_in_this_world = [
location for location in world.multiworld.get_locations(world.player)
if location.item and not location.is_event and location.progress_type != LocationProgressType.EXCLUDED
]
world.random.shuffle(locations_in_this_world)

return prog_items_in_this_world, locations_in_this_world
if world.options.vague_hints:
locations_in_this_world.sort(key=lambda location: cast(Item, location.item).advancement)

location_names_in_this_world = [location.name for location in locations_in_this_world]

return prog_item_names_in_this_world, location_names_in_this_world


def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List["WitnessItem"],
already_hinted_locations: Set[Location]
) -> Tuple[List[WitnessLocationHint], List[WitnessLocationHint]]:
prog_items_in_this_world, loc_in_this_world = get_items_and_locations_in_random_order(world, own_itempool)

always_locations = [
location for location in get_always_hint_locations(world)
if location in loc_in_this_world
]
prog_items_in_this_world, loc_in_this_world = get_item_and_location_names_in_random_order(world, own_itempool)

always_items = [
item for item in get_always_hint_items(world)
if item in prog_items_in_this_world
]
priority_locations = [
location for location in get_priority_hint_locations(world)
if location in loc_in_this_world
]
priority_items = [
item for item in get_priority_hint_items(world)
if item in prog_items_in_this_world
]

if world.options.vague_hints:
always_locations, priority_locations = [], []
else:
always_locations = [
location for location in get_always_hint_locations(world)
if location in loc_in_this_world
]
priority_locations = [
location for location in get_priority_hint_locations(world)
if location in loc_in_this_world
]

# Get always and priority location/item hints
always_location_hints = {hint_from_location(world, location) for location in always_locations}
always_item_hints = {hint_from_item(world, item, own_itempool) for item in always_items}
Expand Down Expand Up @@ -291,7 +373,7 @@ def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List["Wi
def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List["WitnessItem"],
already_hinted_locations: Set[Location], hints_to_use_first: List[WitnessLocationHint],
unhinted_locations_for_hinted_areas: Dict[str, Set[Location]]) -> List[WitnessWordedHint]:
prog_items_in_this_world, locations_in_this_world = get_items_and_locations_in_random_order(world, own_itempool)
prog_items_in_this_world, locations_in_this_world = get_item_and_location_names_in_random_order(world, own_itempool)

next_random_hint_is_location = world.random.randrange(0, 2)

Expand Down Expand Up @@ -384,7 +466,7 @@ def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]]
for region in static_witness_logic.ALL_AREAS_BY_NAME[area]["regions"]
if region in world.player_regions.created_region_names
]
locations = [location for region in regions for location in region.get_locations() if location.address]
locations = [location for region in regions for location in region.get_locations() if not location.is_event]

if locations:
locations_per_area[area] = locations
Expand Down Expand Up @@ -615,9 +697,7 @@ def get_compact_hint_args(hint: WitnessWordedHint, local_player_number: int) ->
"""

# Is Area Hint
if hint.area is not None:
assert hint.area_amount is not None, "Area hint had an undefined progression amount."

if hint.area_amount is not None:
area_amount = hint.area_amount
hunt_panels = hint.area_hunt_panels

Expand All @@ -632,7 +712,11 @@ def get_compact_hint_args(hint: WitnessWordedHint, local_player_number: int) ->

# Is location hint
if location and location.address is not None:
return location.address, location.player
if hint.vague_location_hint and location.player == local_player_number:
assert hint.area is not None # A local vague location hint should have an area argument
return location.address, "containing_area:" + hint.area
else:
return location.address, location.player # Scouting does not matter for other players (currently)

# Is junk / undefined hint
return -1, local_player_number
Expand Down
21 changes: 21 additions & 0 deletions worlds/witness/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,25 @@ class HintAmount(Range):
default = 12


class VagueHints(Choice):
"""Make Location Hints a bit more vague, where they only tell you about the general area the item is in.
Area Hints will be generated as normal.
If set to "stable", only location groups will be used. If location groups aren't implemented for the game your item ended up in, your hint will instead only tell you that the item is "somewhere in" that game.
If set to "experimental", region names will be eligible as well, and you will never receive a "somewhere in" hint. Keep in mind that region names are not always intended to be comprehensible to players — only turn this on if you are okay with a bit of chaos.
The distinction does not matter in single player, as Witness implements location groups for every location.
Also, please don't pester any devs about implementing location groups. Bring it up nicely, accept their response even if it is "No".
"""
display_name = "Vague Hints"

option_off = 0
option_stable = 1
option_experimental = 2


class AreaHintPercentage(Range):
"""
There are two types of hints for The Witness.
Expand Down Expand Up @@ -400,6 +419,7 @@ class TheWitnessOptions(PerGameCommonOptions):
trap_weights: TrapWeights
puzzle_skip_amount: PuzzleSkipAmount
hint_amount: HintAmount
vague_hints: VagueHints
area_hint_percentage: AreaHintPercentage
laser_hints: LaserHints
death_link: DeathLink
Expand Down Expand Up @@ -442,6 +462,7 @@ class TheWitnessOptions(PerGameCommonOptions):
]),
OptionGroup("Hints", [
HintAmount,
VagueHints,
AreaHintPercentage,
LaserHints
]),
Expand Down
4 changes: 2 additions & 2 deletions worlds/witness/test/test_panel_hunt.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_correct_panels_were_picked(self):

for _ in range(100):
state_100.collect(panel_hunt_item, True)
state_100.sweep_for_events(False, [self.world.get_location("Tutorial Gate Open Solved")])
state_100.sweep_for_events([self.world.get_location("Tutorial Gate Open Solved")])

self.assertTrue(self.multiworld.completion_condition[self.player](state_100))

Expand All @@ -33,7 +33,7 @@ def test_correct_panels_were_picked(self):

for _ in range(99):
state_99.collect(panel_hunt_item, True)
state_99.sweep_for_events(False, [self.world.get_location("Tutorial Gate Open Solved")])
state_99.sweep_for_events([self.world.get_location("Tutorial Gate Open Solved")])

self.assertFalse(self.multiworld.completion_condition[self.player](state_99))

Expand Down
1 change: 1 addition & 0 deletions worlds/witness/test/test_roll_other_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class TestMiscOptions(WitnessTestBase):
"laser_hints": True,
"hint_amount": 40,
"area_hint_percentage": 100,
"vague_hints": "experimental",
}


Expand Down

0 comments on commit c4e7b6c

Please sign in to comment.