Files
Grinch-AP/worlds/witness/data/utils.py
NewSoupVi f253dffc07 The Witness: Panel Hunt Mode (#3265)
* Add panel hunt options

* Make sure all panels are either solvable or disabled in panel hunt

* Pick huntable panels

* Discards in disable non randomized

* Set up panel hunt requirement

* Panel hunt functional

* Make it so an event can have multiple names

* Panel hunt with events

* Add hunt entities to slot data

* ruff

* add to hint data, no client sneding yet

* encode panel hunt amount in compact hint data

* Remove print statement

* my b

* consistent

* meh

* additions for lcient

* Nah

* Victory panels ineligible for panel hunt

* Panel Hunt Postgame option

* cleanup

* Add data generation file

* pull out set

* always disable gate ep in panel hunt

* Disallow certain challenge panels from being panel hunt panels

* Make panelhuntpostgame its own function, so it can be called even if normal postgame is enabled

* disallow PP resets from panel hunt

* Disable challenge timer and elevetor start respectively in disable hunt postgame

* Fix panelhunt postgame

* lol

* When you test that the bug is fixed but not that the non-bug is not unfixed

* Prevent Obelisks from being panel hunt panels

* Make picking panels for panel hunt a bit more sophisticated, if less random

* Better function maybe ig

* Ok maybe that was a bit too much

* Give advanced players some control over panel hunt

* lint

* correct the logic for amount to pick

* decided the jingle thing was dumb, I'll figure sth out client side. Same area discouragement is now a configurable factor, and the logic has been significantly rewritten

* comment

* Make the option visible

* Safety

* Change assert slightly

* We do a little logging

* number tweak & we do a lil logging

* we do a little more logging

* Ruff

* Panel Hunt Option Group

* Idk how that got here

* Update worlds/witness/options.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update worlds/witness/__init__.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* remove merge error

* Update worlds/witness/player_logic.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* True

* Don't have underwater sliding bridge when you have above water sliding bridge

* These are not actually connected lol

* get rid of unnecessary variable

* Refactor compact hint function again

* lint

* Pull out Entity Hunt Picking into its own class, split it into many functions. Kept a lot of the comments tho

* forgot to actually add the new file

* some more refactoring & docstrings

* consistent naming

* flip elif change

* Comment about naming

* Make static eligible panels a constant I can refer back to

* slight formatting change

* pull out options-based eligibility into its own function

* better text and stuff

* lint

* this is not necessary

* capitalisation

* Fix same area discouragement 0

* Simplify data file generation

* Simplify data file generation

* prevent div 0

* Add Vault Boxes -> Vault Panels to replacements

* Update options.py

* Update worlds/witness/entity_hunt.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update entity_hunt.py

* Fix some events not working

* assert

* remove now unused function

* lint

* Lasers Activate, Lasers don't Solve

* lint

* oops

* mypy

* lint

* Add simple panel hunt unit test

* Add Panel Hunt Tests

* Add more Panel Hunt Tests

* Disallow Box Short for normal panel hunt

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-08-20 01:16:35 +02:00

262 lines
8.3 KiB
Python

from math import floor
from pkgutil import get_data
from random import Random
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple, TypeVar
T = TypeVar("T")
# A WitnessRule is just an or-chain of and-conditions.
# It represents the set of all options that could fulfill this requirement.
# E.g. if something requires "Dots or (Shapers and Stars)", it'd be represented as: {{"Dots"}, {"Shapers, "Stars"}}
# {} is an unusable requirement.
# {{}} is an always usable requirement.
WitnessRule = FrozenSet[FrozenSet[str]]
def weighted_sample(world_random: Random, population: List[T], weights: List[float], k: int) -> List[T]:
positions = range(len(population))
indices: List[int] = []
while True:
needed = k - len(indices)
if not needed:
break
for i in world_random.choices(positions, weights, k=needed):
if weights[i]:
weights[i] = 0.0
indices.append(i)
return [population[i] for i in indices]
def build_weighted_int_list(inputs: Collection[float], total: int) -> List[int]:
"""
Converts a list of floats to a list of ints of a given length, using the Largest Remainder Method.
"""
# Scale the inputs to sum to the desired total.
scale_factor: float = total / sum(inputs)
scaled_input = [x * scale_factor for x in inputs]
# Generate whole number counts, always rounding down.
rounded_output: List[int] = [floor(x) for x in scaled_input]
rounded_sum = sum(rounded_output)
# If the output's total is insufficient, increment the value that has the largest remainder until we meet our goal.
remainders: List[float] = [real - rounded for real, rounded in zip(scaled_input, rounded_output)]
while rounded_sum < total:
max_remainder = max(remainders)
if max_remainder == 0:
break
# Consume the remainder and increment the total for the given target.
max_remainder_index = remainders.index(max_remainder)
remainders[max_remainder_index] = 0
rounded_output[max_remainder_index] += 1
rounded_sum += 1
return rounded_output
def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str, WitnessRule]]]:
"""
Returns a region object by parsing a line in the logic file
"""
region_string = region_string[:-1]
line_split = region_string.split(" - ")
region_name_full = line_split.pop(0)
region_name_split = region_name_full.split(" (")
region_name = region_name_split[0]
region_name_simple = region_name_split[1][:-1]
options = set()
for _ in range(len(line_split) // 2):
connected_region = line_split.pop(0)
corresponding_lambda = line_split.pop(0)
options.add(
(connected_region, parse_lambda(corresponding_lambda))
)
region_obj = {
"name": region_name,
"shortName": region_name_simple,
"entities": [],
"physical_entities": [],
}
return region_obj, options
def parse_lambda(lambda_string: str) -> WitnessRule:
"""
Turns a lambda String literal like this: a | b & c
into a set of sets like this: {{a}, {b, c}}
The lambda has to be in DNF.
"""
if lambda_string == "True":
return frozenset([frozenset()])
split_ands = set(lambda_string.split(" | "))
return frozenset({frozenset(a.split(" & ")) for a in split_ands})
_adjustment_file_cache = {}
def get_adjustment_file(adjustment_file: str) -> List[str]:
if adjustment_file not in _adjustment_file_cache:
data = get_data(__name__, adjustment_file)
if data is None:
raise FileNotFoundError(f"Could not find {adjustment_file}")
_adjustment_file_cache[adjustment_file] = [line.strip() for line in data.decode("utf-8").split("\n")]
return _adjustment_file_cache[adjustment_file]
def get_disable_unrandomized_list() -> List[str]:
return get_adjustment_file("settings/Exclusions/Disable_Unrandomized.txt")
def get_early_caves_list() -> List[str]:
return get_adjustment_file("settings/Early_Caves.txt")
def get_early_caves_start_list() -> List[str]:
return get_adjustment_file("settings/Early_Caves_Start.txt")
def get_symbol_shuffle_list() -> List[str]:
return get_adjustment_file("settings/Symbol_Shuffle.txt")
def get_complex_doors() -> List[str]:
return get_adjustment_file("settings/Door_Shuffle/Complex_Doors.txt")
def get_simple_doors() -> List[str]:
return get_adjustment_file("settings/Door_Shuffle/Simple_Doors.txt")
def get_complex_door_panels() -> List[str]:
return get_adjustment_file("settings/Door_Shuffle/Complex_Door_Panels.txt")
def get_complex_additional_panels() -> List[str]:
return get_adjustment_file("settings/Door_Shuffle/Complex_Additional_Panels.txt")
def get_simple_panels() -> List[str]:
return get_adjustment_file("settings/Door_Shuffle/Simple_Panels.txt")
def get_simple_additional_panels() -> List[str]:
return get_adjustment_file("settings/Door_Shuffle/Simple_Additional_Panels.txt")
def get_boat() -> List[str]:
return get_adjustment_file("settings/Door_Shuffle/Boat.txt")
def get_laser_shuffle() -> List[str]:
return get_adjustment_file("settings/Laser_Shuffle.txt")
def get_audio_logs() -> List[str]:
return get_adjustment_file("settings/Audio_Logs.txt")
def get_ep_all_individual() -> List[str]:
return get_adjustment_file("settings/EP_Shuffle/EP_All.txt")
def get_ep_obelisks() -> List[str]:
return get_adjustment_file("settings/EP_Shuffle/EP_Sides.txt")
def get_obelisk_keys() -> List[str]:
return get_adjustment_file("settings/Door_Shuffle/Obelisk_Keys.txt")
def get_ep_easy() -> List[str]:
return get_adjustment_file("settings/EP_Shuffle/EP_Easy.txt")
def get_ep_no_eclipse() -> List[str]:
return get_adjustment_file("settings/EP_Shuffle/EP_NoEclipse.txt")
def get_vault_exclusion_list() -> List[str]:
return get_adjustment_file("settings/Exclusions/Vaults.txt")
def get_discard_exclusion_list() -> List[str]:
return get_adjustment_file("settings/Exclusions/Discards.txt")
def get_caves_except_path_to_challenge_exclusion_list() -> List[str]:
return get_adjustment_file("settings/Exclusions/Caves_Except_Path_To_Challenge.txt")
def get_elevators_come_to_you() -> List[str]:
return get_adjustment_file("settings/Door_Shuffle/Elevators_Come_To_You.txt")
def get_entity_hunt() -> List[str]:
return get_adjustment_file("settings/Entity_Hunt.txt")
def get_sigma_normal_logic() -> List[str]:
return get_adjustment_file("WitnessLogic.txt")
def get_sigma_expert_logic() -> List[str]:
return get_adjustment_file("WitnessLogicExpert.txt")
def get_vanilla_logic() -> List[str]:
return get_adjustment_file("WitnessLogicVanilla.txt")
def get_items() -> List[str]:
return get_adjustment_file("WitnessItems.txt")
def optimize_witness_rule(witness_rule: WitnessRule) -> WitnessRule:
"""Removes any redundant terms from a logical formula in disjunctive normal form.
This means removing any terms that are a superset of any other term get removed.
This is possible because of the boolean absorption law: a | (a & b) = a"""
to_remove = set()
for option1 in witness_rule:
for option2 in witness_rule:
if option2 < option1:
to_remove.add(option1)
return witness_rule - to_remove
def logical_and_witness_rules(witness_rules: Iterable[WitnessRule]) -> WitnessRule:
"""
performs the "and" operator on a list of logical formula in disjunctive normal form, represented as a set of sets.
A logical formula might look like this: {{a, b}, {c, d}}, which would mean "a & b | c & d".
These can be easily and-ed by just using the boolean distributive law: (a | b) & c = a & c | a & b.
"""
current_overall_requirement: FrozenSet[FrozenSet[str]] = frozenset({frozenset()})
for next_dnf_requirement in witness_rules:
new_requirement: Set[FrozenSet[str]] = set()
for option1 in current_overall_requirement:
for option2 in next_dnf_requirement:
new_requirement.add(option1 | option2)
current_overall_requirement = frozenset(new_requirement)
return optimize_witness_rule(current_overall_requirement)
def logical_or_witness_rules(witness_rules: Iterable[WitnessRule]) -> WitnessRule:
return optimize_witness_rule(frozenset.union(*witness_rules))