From 860ab10b0bb774d7f716eb3e6db15eb4454f2fef Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 23 May 2024 15:03:21 +0200 Subject: [PATCH] Generate: remove tag "-" (#3036) * Generate: introduce Remove, similar to Merge * make + dict behave as + for each value --------- Co-authored-by: Zach Parks --- Generate.py | 29 ++++++++++++++++---- test/general/test_player_options.py | 2 +- worlds/generic/docs/triggers_en.md | 42 +++++++++++++++++++---------- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/Generate.py b/Generate.py index 2bc061b7..30b99231 100644 --- a/Generate.py +++ b/Generate.py @@ -9,6 +9,7 @@ import urllib.parse import urllib.request from collections import Counter from typing import Any, Dict, Tuple, Union +from itertools import chain import ModuleUpdate @@ -319,18 +320,34 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str logging.debug(f'Applying {new_weights}') cleaned_weights = {} for option in new_weights: - option_name = option.lstrip("+") + option_name = option.lstrip("+-") if option.startswith("+") and option_name in weights: cleaned_value = weights[option_name] new_value = new_weights[option] - if isinstance(new_value, (set, dict)): + if isinstance(new_value, set): cleaned_value.update(new_value) elif isinstance(new_value, list): cleaned_value.extend(new_value) + elif isinstance(new_value, dict): + cleaned_value = dict(Counter(cleaned_value) + Counter(new_value)) else: raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name}," f" received {type(new_value).__name__}.") cleaned_weights[option_name] = cleaned_value + elif option.startswith("-") and option_name in weights: + cleaned_value = weights[option_name] + new_value = new_weights[option] + if isinstance(new_value, set): + cleaned_value.difference_update(new_value) + elif isinstance(new_value, list): + for element in new_value: + cleaned_value.remove(element) + elif isinstance(new_value, dict): + cleaned_value = dict(Counter(cleaned_value) - Counter(new_value)) + else: + raise Exception(f"Cannot apply remove to non-dict, set, or list type {option_name}," + f" received {type(new_value).__name__}.") + cleaned_weights[option_name] = cleaned_value else: cleaned_weights[option_name] = new_weights[option] new_options = set(cleaned_weights) - set(weights) @@ -466,9 +483,11 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b world_type = AutoWorldRegister.world_types[ret.game] game_weights = weights[ret.game] - if any(weight.startswith("+") for weight in game_weights) or \ - any(weight.startswith("+") for weight in weights): - raise Exception(f"Merge tag cannot be used outside of trigger contexts.") + for weight in chain(game_weights, weights): + if weight.startswith("+"): + raise Exception(f"Merge tag cannot be used outside of trigger contexts. Found {weight}") + if weight.startswith("-"): + raise Exception(f"Remove tag cannot be used outside of trigger contexts. Found {weight}") if "triggers" in game_weights: weights = roll_triggers(weights, game_weights["triggers"], valid_trigger_names) diff --git a/test/general/test_player_options.py b/test/general/test_player_options.py index 9650fbe9..ea7f19e3 100644 --- a/test/general/test_player_options.py +++ b/test/general/test_player_options.py @@ -31,7 +31,7 @@ class TestPlayerOptions(unittest.TestCase): self.assertEqual(new_weights["list_2"], ["string_3"]) self.assertEqual(new_weights["list_1"], ["string", "string_2"]) self.assertEqual(new_weights["dict_1"]["option_a"], 50) - self.assertEqual(new_weights["dict_1"]["option_b"], 0) + self.assertEqual(new_weights["dict_1"]["option_b"], 50) self.assertEqual(new_weights["dict_1"]["option_c"], 50) self.assertNotIn("option_f", new_weights["dict_2"]) self.assertEqual(new_weights["dict_2"]["option_g"], 50) diff --git a/worlds/generic/docs/triggers_en.md b/worlds/generic/docs/triggers_en.md index 73cca665..c084b53f 100644 --- a/worlds/generic/docs/triggers_en.md +++ b/worlds/generic/docs/triggers_en.md @@ -123,10 +123,21 @@ again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard` and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery". -Options that define a list, set, or dict can additionally have the character `+` added to the start of their name, which applies the contents of -the activated trigger to the already present equivalents in the game options. +## Adding or Removing from a List, Set, or Dict Option + +List, set, and dict options can additionally have values added to or removed from itself without overriding the existing +option value by prefixing the option name in the trigger block with `+` (add) or `-` (remove). The exact behavior for +each will depend on the option type. + +- For sets, `+` will add the value(s) to the set and `-` will remove any value(s) of the set. Sets do not allow + duplicates. +- For lists, `+` will add new values(s) to the list and `-` will remove the first matching values(s) it comes across. + Lists allow duplicate values. +- For dicts, `+` will add the value(s) to the given key(s) inside the dict if it exists, or add it otherwise. `-` is the + inverse operation of addition (and negative values are allowed). For example: + ```yaml Super Metroid: start_location: @@ -134,18 +145,21 @@ Super Metroid: aqueduct: 50 start_hints: - Morph Ball -triggers: - - option_category: Super Metroid - option_name: start_location - option_result: aqueduct - options: - Super Metroid: - +start_hints: - - Gravity Suit + start_inventory: + Power Bombs: 1 + triggers: + - option_category: Super Metroid + option_name: start_location + option_result: aqueduct + options: + Super Metroid: + +start_hints: + - Gravity Suit ``` -In this example, if the `start_location` option rolls `landing_site`, only a starting hint for Morph Ball will be created. -If `aqueduct` is rolled, a starting hint for Gravity Suit will also be created alongside the hint for Morph Ball. +In this example, if the `start_location` option rolls `landing_site`, only a starting hint for Morph Ball will be +created. If `aqueduct` is rolled, a starting hint for Gravity Suit will also be created alongside the hint for Morph +Ball. -Note that for lists, items can only be added, not removed or replaced. For dicts, defining a value for a present key will -replace that value within the dict. +Note that for lists, items can only be added, not removed or replaced. For dicts, defining a value for a present key +will replace that value within the dict.