From 9ab7c8d9e5b18a6f8e8eed7ed5eb2c489f202860 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 9 May 2022 07:20:28 +0200 Subject: [PATCH] Witness: Changes in response to Beta run 1 (#494) Co-authored-by: metzner --- worlds/witness/Options.py | 17 +++++++++---- worlds/witness/WitnessLogic.txt | 2 +- worlds/witness/__init__.py | 20 +++++++++------- worlds/witness/items.py | 40 ++++++++++++++++++++++++++----- worlds/witness/utils.py | 42 ++++++++++++++++++++++++++++++++- 5 files changed, 100 insertions(+), 21 deletions(-) diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index c150225d..ce590810 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -1,6 +1,6 @@ from typing import Dict from BaseClasses import MultiWorld -from Options import Toggle, DefaultOnToggle, Option +from Options import Toggle, DefaultOnToggle, Option, Range # class HardMode(Toggle): @@ -30,8 +30,8 @@ class ShuffleVaultBoxes(Toggle): class ShuffleUncommonLocations(Toggle): - """Adds the following checks to the pool: - Mountaintop River Shape, Tutorial Patio Floor, Theater Videos""" + """Adds some optional puzzles that are somewhat difficult or out of the way. + Examples: Mountaintop River Shape, Tutorial Patio Floor, Theater Videos""" display_name = "Shuffle Uncommon Locations" @@ -46,6 +46,14 @@ class ChallengeVictoryCondition(Toggle): display_name = "Victory on beating the Challenge" +class TrapPercentage(Range): + """Replaces junk items with traps, at the specified rate.""" + display_name = "Trap Percentage" + range_start = 0 + range_end = 100 + default = 20 + + the_witness_options: Dict[str, Option] = { # "hard_mode": HardMode, # "unlock_symbols": UnlockSymbols, @@ -54,7 +62,8 @@ the_witness_options: Dict[str, Option] = { "shuffle_vault_boxes": ShuffleVaultBoxes, "shuffle_uncommon": ShuffleUncommonLocations, "shuffle_hard": ShuffleHardLocations, - "challenge_victory": ChallengeVictoryCondition + "challenge_victory": ChallengeVictoryCondition, + "trap_percentage": TrapPercentage } diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index 5a9d9de5..e17f1333 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -370,7 +370,7 @@ Jungle (Jungle) - Main Island - True: Outside Jungle River (River) - Main Island - True - Jungle - 0x337FA: 0x17CAA (Rhombic Avoid to Monastery Garden) - True - Environment -0x15ADD (Rhombic Avoid Vault) - True - Environment +0x15ADD (Vault) - True - Environment & Black/White Squares & Dots 0x03702 (Vault Box) - 0x15ADD - True Outside Bunker (Bunker) - Main Island - True - Inside Bunker - 0x0A079: diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 1c10c6bc..6d02e774 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -12,6 +12,7 @@ from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems from .rules import set_rules from .regions import WitnessRegions from .Options import is_option_enabled, the_witness_options +from .utils import best_junk_to_add_based_on_weights class WitnessWebWorld(WebWorld): @@ -50,6 +51,8 @@ class WitnessWorld(World): self.items = WitnessPlayerItems(self.locat, self.world, self.player, self.player_logic) self.regio = WitnessRegions(self.locat) + self.junk_items_created = {key: 0 for key in self.items.JUNK_WEIGHTS.keys()} + def generate_basic(self): # Generate item pool pool = [] @@ -69,13 +72,10 @@ class WitnessWorld(World): pool.remove(items_by_name[random_good_item]) # Put in junk items to fill the rest - junk_pool = self.items.JUNK_WEIGHTS.copy() - junk_pool = self.world.random.choices( - list(junk_pool.keys()), weights=list(junk_pool.values()), - k=len(self.locat.CHECK_LOCATION_TABLE) - len(pool) - len(self.locat.EVENT_LOCATION_TABLE) - 1 - ) + junk_size = len(self.locat.CHECK_LOCATION_TABLE) - len(pool) - len(self.locat.EVENT_LOCATION_TABLE) - 1 - pool += [self.create_item(junk) for junk in junk_pool] + for i in range(0, junk_size): + pool.append(self.create_item(self.get_filler_item_name())) # Tie Event Items to Event Locations (e.g. Laser Activations) for event_location in self.locat.EVENT_LOCATION_TABLE: @@ -118,10 +118,12 @@ class WitnessWorld(World): new_item.trap = item.trap return new_item - def get_filler_item_name(self) -> str: # Used ny itemlinks - junk_pool = self.items.JUNK_WEIGHTS.copy() + def get_filler_item_name(self) -> str: # Used by itemlinks + item = best_junk_to_add_based_on_weights(self.items.JUNK_WEIGHTS, self.junk_items_created) - return self.world.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()))[0] + self.junk_items_created[item] += 1 + + return item class WitnessLocation(Location): diff --git a/worlds/witness/items.py b/worlds/witness/items.py index 4e19bfa7..47bbd1fb 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -6,7 +6,8 @@ from typing import Dict, NamedTuple, Optional from BaseClasses import Item, MultiWorld from . import StaticWitnessLogic, WitnessPlayerLocations, WitnessPlayerLogic -from .Options import is_option_enabled +from .Options import get_option_value, is_option_enabled +from fractions import Fraction class ItemData(NamedTuple): @@ -33,12 +34,19 @@ class StaticWitnessItems: ALL_ITEM_TABLE: Dict[str, ItemData] = {} - JUNK_WEIGHTS = { - "Speed Boost": 1, - "Slowness": 0.8, - "Power Surge": 0.2, + # These should always add up to 1!!! + BONUS_WEIGHTS = { + "Speed Boost": Fraction(1, 1), } + # These should always add up to 1!!! + TRAP_WEIGHTS = { + "Slowness": Fraction(8, 10), + "Power Surge": Fraction(2, 10), + } + + ALL_JUNK_ITEMS = set(BONUS_WEIGHTS.keys()) | set(TRAP_WEIGHTS.keys()) + def __init__(self): item_tab = dict() @@ -91,8 +99,28 @@ class WitnessPlayerItems: self.EVENT_ITEM_TABLE[location] = ItemData(None, True, True) self.ITEM_TABLE[location] = ItemData(None, True, True) + trap_percentage = get_option_value(world, player, "trap_percentage") + + self.JUNK_WEIGHTS = dict() + + if trap_percentage != 0: + # I'm sure there must be some super "pythonic" way of doing this :D + + for trap_name, trap_weight in StaticWitnessItems.TRAP_WEIGHTS.items(): + self.JUNK_WEIGHTS[trap_name] = (trap_weight * trap_percentage) / 100 + + if trap_percentage != 100: + for bonus_name, bonus_weight in StaticWitnessItems.BONUS_WEIGHTS.items(): + self.JUNK_WEIGHTS[bonus_name] = (bonus_weight * (100 - trap_percentage)) / 100 + self.JUNK_WEIGHTS = { key: value for (key, value) - in StaticWitnessItems.JUNK_WEIGHTS.items() + in self.JUNK_WEIGHTS.items() if key in self.ITEM_TABLE.keys() } + + # JUNK_WEIGHTS will add up to 1 if the boosts weights and the trap weights each add up to 1 respectively. + + for junk_item in StaticWitnessItems.ALL_JUNK_ITEMS: + if junk_item not in self.JUNK_WEIGHTS.keys(): + del self.ITEM_TABLE[junk_item] diff --git a/worlds/witness/utils.py b/worlds/witness/utils.py index 85f43ab2..3ccaf5d1 100644 --- a/worlds/witness/utils.py +++ b/worlds/witness/utils.py @@ -1,5 +1,45 @@ import os from Utils import cache_argsless +from itertools import accumulate +from typing import * +from fractions import Fraction + + +def best_junk_to_add_based_on_weights(weights: Dict[Any, Fraction], created_junk: Dict[Any, int]): + min_error = ("", 2) + + for junk_name, instances in created_junk.items(): + new_dist = created_junk.copy() + new_dist[junk_name] += 1 + new_dist_length = sum(new_dist.values()) + new_dist = {key: Fraction(value/1)/new_dist_length for key, value in new_dist.items()} + + errors = {key: abs(new_dist[key] - weights[key]) for key in created_junk.keys()} + + new_min_error = max(errors.values()) + + if min_error[1] > new_min_error: + min_error = (junk_name, new_min_error) + + return min_error[0] + + +def weighted_list(weights: Dict[Any, Fraction], length): + """ + Example: + weights = {A: 0.3, B: 0.3, C: 0.4} + length = 10 + + returns: [A, A, A, B, B, B, C, C, C, C] + + Makes sure to match length *exactly*, might approximate as a result + """ + vals = accumulate(map(lambda x: x * length, weights.values()), lambda x, y: x + y) + output_list = [] + for k, v in zip(weights.keys(), vals): + while len(output_list) < v: + output_list.append(k) + return output_list def define_new_region(region_string): @@ -55,4 +95,4 @@ def get_disable_unrandomized_list(): path = os.path.join(os.path.dirname(__file__), adjustment_file) with open(path) as f: - return [line.strip() for line in f.readlines()] \ No newline at end of file + return [line.strip() for line in f.readlines()]