The Witness: Add "vague" hints making use of other games' region names 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 <gyroscope15@gmail.com>

* Update worlds/witness/hints.py

Co-authored-by: Bryce Wilson <gyroscope15@gmail.com>

* 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 <gyroscope15@gmail.com>

* 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 <scipiowright@gmail.com>

* 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 <gyroscope15@gmail.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
This commit is contained in:
NewSoupVi
2024-08-20 01:34:40 +02:00
committed by GitHub
parent f253dffc07
commit c4e7b6ca82
4 changed files with 142 additions and 36 deletions

View File

@@ -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
@@ -11,8 +12,8 @@ from .player_items import WitnessItem
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
@@ -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]:
@@ -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:
@@ -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
return WitnessWordedHint(hint_text, hint.location)
if world.options.vague_hints:
chosen_group, group_type = try_getting_location_group_for_location(world, 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,
@@ -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}
@@ -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)
@@ -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
@@ -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
@@ -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