From 5d9d4ed9f1e44309f1b53f12413ad260f1b6c983 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 29 Mar 2024 01:01:31 +0100 Subject: [PATCH] SoE: update to pyevermizer v0.48.0 (#3050) --- worlds/soe/__init__.py | 52 ++++++++--- worlds/soe/logic.py | 7 +- worlds/soe/options.py | 25 +++++- worlds/soe/requirements.txt | 72 +++++++-------- worlds/soe/test/__init__.py | 2 + worlds/soe/test/test_sniffamizer.py | 130 ++++++++++++++++++++++++++++ 6 files changed, 237 insertions(+), 51 deletions(-) create mode 100644 worlds/soe/test/test_sniffamizer.py diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index bbe018da5329..dcca722ad1fe 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -13,7 +13,7 @@ from worlds.AutoWorld import WebWorld, World from worlds.generic.Rules import add_item_rule, set_rule from .logic import SoEPlayerLogic -from .options import Difficulty, EnergyCore, SoEOptions +from .options import Difficulty, EnergyCore, Sniffamizer, SniffIngredients, SoEOptions from .patch import SoEDeltaPatch, get_base_rom_path if typing.TYPE_CHECKING: @@ -64,20 +64,28 @@ pyevermizer.CHECK_BOSS: _id_base + 50, # bosses 64050..6499 pyevermizer.CHECK_GOURD: _id_base + 100, # gourds 64100..64399 pyevermizer.CHECK_NPC: _id_base + 400, # npc 64400..64499 - # TODO: sniff 64500..64799 + # blank 64500..64799 pyevermizer.CHECK_EXTRA: _id_base + 800, # extra items 64800..64899 pyevermizer.CHECK_TRAP: _id_base + 900, # trap 64900..64999 + pyevermizer.CHECK_SNIFF: _id_base + 1000 # sniff 65000..65592 } # cache native evermizer items and locations _items = pyevermizer.get_items() +_sniff_items = pyevermizer.get_sniff_items() # optional, not part of the default location pool _traps = pyevermizer.get_traps() _extras = pyevermizer.get_extra_items() # items that are not placed by default _locations = pyevermizer.get_locations() +_sniff_locations = pyevermizer.get_sniff_locations() # optional, not part of the default location pool # fix up texts for AP for _loc in _locations: if _loc.type == pyevermizer.CHECK_GOURD: - _loc.name = f'{_loc.name} #{_loc.index}' + _loc.name = f"{_loc.name} #{_loc.index}" +for _loc in _sniff_locations: + if _loc.type == pyevermizer.CHECK_SNIFF: + _loc.name = f"{_loc.name} Sniff #{_loc.index}" +del _loc + # item helpers _ingredients = ( 'Wax', 'Water', 'Vinegar', 'Root', 'Oil', 'Mushroom', 'Mud Pepper', 'Meteorite', 'Limestone', 'Iron', @@ -97,7 +105,7 @@ def _match_item_name(item: pyevermizer.Item, substr: str) -> bool: def _get_location_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Location]]: name_to_id = {} id_to_raw = {} - for loc in _locations: + for loc in itertools.chain(_locations, _sniff_locations): ap_id = _id_offset[loc.type] + loc.index id_to_raw[ap_id] = loc name_to_id[loc.name] = ap_id @@ -108,7 +116,7 @@ def _get_location_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[i def _get_item_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Item]]: name_to_id = {} id_to_raw = {} - for item in itertools.chain(_items, _extras, _traps): + for item in itertools.chain(_items, _sniff_items, _extras, _traps): if item.name in name_to_id: continue ap_id = _id_offset[item.type] + item.index @@ -168,9 +176,9 @@ class SoEWorld(World): options: SoEOptions settings: typing.ClassVar[SoESettings] topology_present = False - data_version = 4 + data_version = 5 web = SoEWebWorld() - required_client_version = (0, 3, 5) + required_client_version = (0, 4, 4) item_name_to_id, item_id_to_raw = _get_item_mapping() location_name_to_id, location_id_to_raw = _get_location_mapping() @@ -238,16 +246,26 @@ def get_sphere_index(evermizer_loc: pyevermizer.Location) -> int: spheres.setdefault(get_sphere_index(loc), {}).setdefault(loc.type, []).append( SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], ingame, loc.difficulty > max_difficulty)) + # extend pool if feature and setting enabled + if hasattr(Sniffamizer, "option_everywhere") and self.options.sniffamizer == Sniffamizer.option_everywhere: + for loc in _sniff_locations: + spheres.setdefault(get_sphere_index(loc), {}).setdefault(loc.type, []).append( + SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], ingame, + loc.difficulty > max_difficulty)) # location balancing data trash_fills: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int, int]]] = { - 0: {pyevermizer.CHECK_GOURD: (20, 40, 40, 40)}, # remove up to 40 gourds from sphere 1 - 1: {pyevermizer.CHECK_GOURD: (70, 90, 90, 90)}, # remove up to 90 gourds from sphere 2 + 0: {pyevermizer.CHECK_GOURD: (20, 40, 40, 40), # remove up to 40 gourds from sphere 1 + pyevermizer.CHECK_SNIFF: (100, 130, 130, 130)}, # remove up to 130 sniff spots from sphere 1 + 1: {pyevermizer.CHECK_GOURD: (70, 90, 90, 90), # remove up to 90 gourds from sphere 2 + pyevermizer.CHECK_SNIFF: (160, 200, 200, 200)}, # remove up to 200 sniff spots from sphere 2 } # mark some as excluded based on numbers above for trash_sphere, fills in trash_fills.items(): for typ, counts in fills.items(): + if typ not in spheres[trash_sphere]: + continue # e.g. player does not have sniff locations count = counts[self.options.difficulty.value] for location in self.random.sample(spheres[trash_sphere][typ], count): assert location.name != "Energy Core #285", "Error in sphere generation" @@ -299,6 +317,15 @@ def create_items(self) -> None: # remove one pair of wings that will be placed in generate_basic items.remove(self.create_item("Wings")) + # extend pool if feature and setting enabled + if hasattr(Sniffamizer, "option_everywhere") and self.options.sniffamizer == Sniffamizer.option_everywhere: + if self.options.sniff_ingredients == SniffIngredients.option_vanilla_ingredients: + # vanilla ingredients + items += list(map(lambda item: self.create_item(item), _sniff_items)) + else: + # random ingredients + items += [self.create_item(self.get_filler_item_name()) for _ in _sniff_items] + def is_ingredient(item: pyevermizer.Item) -> bool: for ingredient in _ingredients: if _match_item_name(item, ingredient): @@ -345,7 +372,12 @@ def set_rules(self) -> None: set_rule(self.multiworld.get_location('Done', self.player), lambda state: self.logic.has(state, pyevermizer.P_FINAL_BOSS)) set_rule(self.multiworld.get_entrance('New Game', self.player), lambda state: True) - for loc in _locations: + locations: typing.Iterable[pyevermizer.Location] + if hasattr(Sniffamizer, "option_everywhere") and self.options.sniffamizer == Sniffamizer.option_everywhere: + locations = itertools.chain(_locations, _sniff_locations) + else: + locations = _locations + for loc in locations: location = self.multiworld.get_location(loc.name, self.player) set_rule(location, self.make_rule(loc.requires)) diff --git a/worlds/soe/logic.py b/worlds/soe/logic.py index ee81c76e58de..92ffb14b3f95 100644 --- a/worlds/soe/logic.py +++ b/worlds/soe/logic.py @@ -1,4 +1,5 @@ import typing +from itertools import chain from typing import Callable, Set from . import pyevermizer @@ -11,10 +12,12 @@ # TODO: resolve/flatten/expand rules to get rid of recursion below where possible # Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items) -rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0] +rules = pyevermizer.get_logic() # Logic.items are all items and extra items excluding non-progression items and duplicates +# NOTE: we are skipping sniff items here because none of them is supposed to provide progression item_names: Set[str] = set() -items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items()) +items = [item for item in filter(lambda item: item.progression, # type: ignore[arg-type] + chain(pyevermizer.get_items(), pyevermizer.get_extra_items())) if item.name not in item_names and not item_names.add(item.name)] # type: ignore[func-returns-value] diff --git a/worlds/soe/options.py b/worlds/soe/options.py index c5ac02c22d03..5ecd0f9e6666 100644 --- a/worlds/soe/options.py +++ b/worlds/soe/options.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, fields +from datetime import datetime from typing import Any, ClassVar, cast, Dict, Iterator, List, Tuple, Protocol from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Option, PerGameCommonOptions, \ @@ -158,13 +159,30 @@ class Ingredienizer(EvermizerFlags, OffOnFullChoice): flags = ['i', '', 'I'] -class Sniffamizer(EvermizerFlags, OffOnFullChoice): - """On Shuffles, Full randomizes drops in sniff locations""" +class Sniffamizer(EvermizerFlags, Choice): + """ + Off: all vanilla items in sniff spots + Shuffle: sniff items shuffled into random sniff spots + """ display_name = "Sniffamizer" + option_off = 0 + option_shuffle = 1 + if datetime.today().year > 2024 or datetime.today().month > 3: + option_everywhere = 2 + __doc__ = __doc__ + " Everywhere: add sniff spots to multiworld pool" + alias_true = 1 default = 1 flags = ['s', '', 'S'] +class SniffIngredients(EvermizerFlag, Choice): + """Select which items should be used as sniff items""" + display_name = "Sniff Ingredients" + option_vanilla_ingredients = 0 + option_random_ingredients = 1 + flag = 'v' + + class Callbeadamizer(EvermizerFlags, OffOnFullChoice): """On Shuffles call bead characters, Full shuffles individual spells""" display_name = "Callbeadamizer" @@ -207,7 +225,7 @@ def __new__(mcs, name: str, bases: Tuple[type], attrs: Dict[Any, Any]) -> "ItemC attrs["display_name"] = f"{attrs['item_name']} Chance" attrs["range_start"] = 0 attrs["range_end"] = 100 - cls = super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs) + cls = super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs) # type: ignore[no-untyped-call] return cast(ItemChanceMeta, cls) @@ -268,6 +286,7 @@ class SoEOptions(PerGameCommonOptions): short_boss_rush: ShortBossRush ingredienizer: Ingredienizer sniffamizer: Sniffamizer + sniff_ingredients: SniffIngredients callbeadamizer: Callbeadamizer musicmizer: Musicmizer doggomizer: Doggomizer diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt index 710f51ddb09a..4bcacb33c33c 100644 --- a/worlds/soe/requirements.txt +++ b/worlds/soe/requirements.txt @@ -1,36 +1,36 @@ -pyevermizer==0.46.1 \ - --hash=sha256:9fd71b5e4af26a5dd24a9cbf5320bf0111eef80320613401a1c03011b1515806 \ - --hash=sha256:23f553ed0509d9a238b2832f775e0b5abd7741b38ab60d388294ee8a7b96c5fb \ - --hash=sha256:7189b67766418a3e7e6c683f09c5e758aa1a5c24316dd9b714984bac099c4b75 \ - --hash=sha256:befa930711e63d5d5892f67fd888b2e65e746363e74599c53e71ecefb90ae16a \ - --hash=sha256:202933ce21e0f33859537bf3800d9a626c70262a9490962e3f450171758507ca \ - --hash=sha256:c20ca69311c696528e1122ebc7d33775ee971f538c0e3e05dd3bfd4de10b82d4 \ - --hash=sha256:74dc689a771ae5ffcd5257e763f571ee890e3e87bdb208233b7f451522c00d66 \ - --hash=sha256:072296baef464daeb6304cf58827dcbae441ad0803039aee1c0caa10d56e0674 \ - --hash=sha256:7921baf20d52d92d6aeb674125963c335b61abb7e1298bde4baf069d11a2d05e \ - --hash=sha256:ca098034a84007038c2bff004582e6e6ac2fa9cc8b9251301d25d7e2adcee6da \ - --hash=sha256:22ddb29823c19be9b15e1b3627db1babfe08b486aede7d5cc463a0a1ae4c75d8 \ - --hash=sha256:bf1c441b49026d9000166be6e2f63fc351a3fda170aa3fdf18d44d5e5d044640 \ - --hash=sha256:9710aa7957b4b1f14392006237eb95803acf27897377df3e85395f057f4316b9 \ - --hash=sha256:8feb676c198bee17ab991ee015828345ac3f87c27dfdb3061d92d1fe47c184b4 \ - --hash=sha256:597026dede72178ff3627a4eb3315de8444461c7f0f856f5773993c3f9790c53 \ - --hash=sha256:70f9b964bdfb5191e8f264644c5d1af3041c66fe15261df8a99b3d719dc680d6 \ - --hash=sha256:74655c0353ffb6cda30485091d0917ce703b128cd824b612b3110a85c79a93d0 \ - --hash=sha256:0e9c74d105d4ec3af12404e85bb8776931c043657add19f798ee69465f92b999 \ - --hash=sha256:d3c13446d3d482b9cce61ac73b38effd26fcdcf7f693a405868d3aaaa4d18ca6 \ - --hash=sha256:371ac3360640ef439a5920ddfe11a34e9d2e546ed886bb8c9ed312611f9f4655 \ - --hash=sha256:6e5cf63b036f24d2ae4375a88df8d0bc93208352939521d1fcac3c829ef2c363 \ - --hash=sha256:edf28f5c4d1950d17343adf6d8d40d12c7e982d1e39535d55f7915e122cd8b0e \ - --hash=sha256:b5ef6f3b4e04f677c296f60f7f4c320ac22cd5bc09c05574460116c8641c801a \ - --hash=sha256:dd651f66720af4abe2ddae29944e299a57ff91e6fca1739e6dc1f8fd7a8c2b39 \ - --hash=sha256:4e278f5f72c27f9703bce5514d2fead8c00361caac03e94b0bf9ad8a144f1eeb \ - --hash=sha256:38f36ea1f545b835c3ecd6e081685a233ac2e3cf0eec8916adc92e4d791098a6 \ - --hash=sha256:0a2e58ed6e7c42f006cc17d32cec1f432f01b3fe490e24d71471b36e0d0d8742 \ - --hash=sha256:c1b658db76240596c03571c60635abe953f36fb55b363202971831c2872ea9a0 \ - --hash=sha256:deb5a84a6a56325eb6701336cdbf70f72adaaeab33cbe953d0e551ecf2592f20 \ - --hash=sha256:b1425c793e0825f58b3726e7afebaf5a296c07cb0d28580d0ee93dbe10dcdf63 \ - --hash=sha256:11995fb4dfd14b5c359591baee2a864c5814650ba0084524d4ea0466edfaf029 \ - --hash=sha256:5d2120b5c93ae322fe2a85d48e3eab4168a19e974a880908f1ac291c0300940f \ - --hash=sha256:254912ea4bfaaffb0abe366e73bd9ecde622677d6afaf2ce8a0c330df99fefd9 \ - --hash=sha256:540d8e4525f0b5255c1554b4589089dc58e15df22f343e9545ea00f7012efa07 \ - --hash=sha256:f69b8ebded7eed181fabe30deabae89fd10c41964f38abb26b19664bbe55c1ae +pyevermizer==0.48.0 \ + --hash=sha256:069ce348e480e04fd6208cfd0f789c600b18d7c34b5272375b95823be191ed57 \ + --hash=sha256:58164dddaba2f340b0a8b4f39605e9dac46d8b0ffb16120e2e57bef2bfc1d683 \ + --hash=sha256:115dd09d38a10f11d4629b340dfd75e2ba4089a1ff9e9748a11619829e02c876 \ + --hash=sha256:b5e79cfe721e75cd7dec306b5eecd6385ce059e31ef7523ba7f677e22161ec6f \ + --hash=sha256:382882fa9d641b9969a6c3ed89449a814bdabcb6b17b558872d95008a6cc908b \ + --hash=sha256:92f67700e9132064a90858d391dd0b8fb111aff6dfd472befed57772d89ae567 \ + --hash=sha256:fe4c453b7dbd5aa834b81f9a7aedb949a605455650b938b8b304d8e5a7edcbf7 \ + --hash=sha256:c6bdbc45daf73818f763ed59ad079f16494593395d806f772dd62605c722b3e9 \ + --hash=sha256:bb09f45448fdfd28566ae6fcc38c35a6632f4c31a9de2483848f6ce17b2359b5 \ + --hash=sha256:00a8b9014744bd1528d0d39c33ede7c0d1713ad797a331cebb33d377a5bc1064 \ + --hash=sha256:64ee69edc0a7d3b3caded78f2e46975f9beaff1ff8feaf29b87da44c45f38d7d \ + --hash=sha256:9211bdb1313e9f4869ed5bdc61f3831d39679bd08bb4087f1c1e5475d9e3018b \ + --hash=sha256:4a57821e422a1d75fe3307931a78db7a65e76955f8e401c4b347db6570390d09 \ + --hash=sha256:04670cee0a0b913f24d2b9a1e771781560e2485bda31e6cd372a08421cf85cfa \ + --hash=sha256:971fe77d0a20a1db984020ad253b613d0983f5e23ff22cba60ee5ac00d8128de \ + --hash=sha256:127265fdb49f718f54706bf15604af1cec23590afd00d423089dea4331dcfc61 \ + --hash=sha256:d47576360337c1a23f424cd49944a8d68fc4f3338e00719c9f89972c84604bef \ + --hash=sha256:879659603e51130a0de8d9885d815a2fa1df8bd6cebe6d520d1c6002302adfdb \ + --hash=sha256:6a91bfc53dd130db6424adf8ac97a1133e97b4157ed00f889d8cbd26a2a4b340 \ + --hash=sha256:f3bf35fc5eef4cda49d2de77339fc201dd3206660a3dc15db005625b15bb806c \ + --hash=sha256:e7c8d5bf59a3c16db20411bc5d8e9c9087a30b6b4edf1b5ed9f4c013291427e4 \ + --hash=sha256:054a4d84ffe75448d41e88e1e0642ef719eb6111be5fe608e71e27a558c59069 \ + --hash=sha256:e6f141ca367469c69ba7fbf65836c479ec6672c598cfcb6b39e8098c60d346bc \ + --hash=sha256:6e65eb88f0c1ff4acde1c13b24ce649b0fe3d1d3916d02d96836c781a5022571 \ + --hash=sha256:e61e8f476b6da809cf38912755ed8bb009665f589e913eb8df877e9fa763024b \ + --hash=sha256:7e7c5484c0a2e3da6064de3f73d8d988d6703db58ab0be4730cbbf1a82319237 \ + --hash=sha256:9033b954e5f4878fd94af6d2056c78e3316115521fb1c24a4416d5cbf2ad66ad \ + --hash=sha256:824c623fff8ae4da176306c458ad63ad16a06a495a16db700665eca3c115924f \ + --hash=sha256:8e31031409a8386c6a63b79d480393481badb3ba29f32ff7a0db2b4abed20ac8 \ + --hash=sha256:7dbb7bb13e1e94f69f7ccdbcf4d35776424555fce5af1ca29d0256f91fdf087a \ + --hash=sha256:3a24e331b259407b6912d6e0738aa8a675831db3b7493fcf54dc17cb0cb80d37 \ + --hash=sha256:fdda06662a994271e96633cba100dd92b2fcd524acef8b2f664d1aaa14503cbd \ + --hash=sha256:0f0fc81bef3dbb78ba6a7622dd4296f23c59825968a0bb0448beb16eb3397cc2 \ + --hash=sha256:e07cbef776a7468669211546887357cc88e9afcf1578b23a4a4f2480517b15d9 \ + --hash=sha256:e442212695bdf60e455673b7b9dd83a5d4b830d714376477093d2c9054d92832 diff --git a/worlds/soe/test/__init__.py b/worlds/soe/test/__init__.py index 1ab852163053..e84d7e669d2c 100644 --- a/worlds/soe/test/__init__.py +++ b/worlds/soe/test/__init__.py @@ -1,9 +1,11 @@ from test.bases import WorldTestBase from typing import Iterable +from .. import SoEWorld class SoETestBase(WorldTestBase): game = "Secret of Evermore" + world: SoEWorld def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: Iterable[str] = (), satisfied: bool = True) -> None: diff --git a/worlds/soe/test/test_sniffamizer.py b/worlds/soe/test/test_sniffamizer.py new file mode 100644 index 000000000000..45aff1fa4d6d --- /dev/null +++ b/worlds/soe/test/test_sniffamizer.py @@ -0,0 +1,130 @@ +import typing +from unittest import TestCase, skipUnless + +from . import SoETestBase +from .. import pyevermizer +from ..options import Sniffamizer + + +class TestCount(TestCase): + """ + Test that counts line up for sniff spots + """ + + def test_compare_counts(self) -> None: + self.assertEqual(len(pyevermizer.get_sniff_locations()), len(pyevermizer.get_sniff_items()), + "Sniff locations and sniff items don't line up") + + +class Bases: + # class in class to avoid running tests for helper class + class TestSniffamizerLocal(SoETestBase): + """ + Test that provided options do not add sniff items or locations + """ + def test_no_sniff_items(self) -> None: + self.assertLess(len(self.multiworld.itempool), 500, + "Unexpected number of items") + for item in self.multiworld.itempool: + if item.code is not None: + self.assertLess(item.code, 65000, + "Unexpected item type") + + def test_no_sniff_locations(self) -> None: + location_count = sum(1 for location in self.multiworld.get_locations(self.player) if location.item is None) + self.assertLess(location_count, 500, + "Unexpected number of locations") + for location in self.multiworld.get_locations(self.player): + if location.address is not None: + self.assertLess(location.address, 65000, + "Unexpected location type") + self.assertEqual(location_count, len(self.multiworld.itempool), + "Locations and item counts do not line up") + + class TestSniffamizerPool(SoETestBase): + """ + Test that provided options add sniff items and locations + """ + def test_sniff_items(self) -> None: + self.assertGreater(len(self.multiworld.itempool), 500, + "Unexpected number of items") + + def test_sniff_locations(self) -> None: + location_count = sum(1 for location in self.multiworld.get_locations(self.player) if location.item is None) + self.assertGreater(location_count, 500, + "Unexpected number of locations") + self.assertTrue(any(location.address is not None and location.address >= 65000 + for location in self.multiworld.get_locations(self.player)), + "No sniff locations") + self.assertEqual(location_count, len(self.multiworld.itempool), + "Locations and item counts do not line up") + + +class TestSniffamizerShuffle(Bases.TestSniffamizerLocal): + """ + Test that shuffle does not add extra items or locations + """ + options: typing.Dict[str, typing.Any] = { + "sniffamizer": "shuffle" + } + + def test_flags(self) -> None: + # default -> no flags + flags = self.world.options.flags + self.assertNotIn("s", flags) + self.assertNotIn("S", flags) + self.assertNotIn("v", flags) + + +@skipUnless(hasattr(Sniffamizer, "option_everywhere"), "Feature disabled") +class TestSniffamizerEverywhereVanilla(Bases.TestSniffamizerPool): + """ + Test that everywhere + vanilla ingredients does add extra items and locations + """ + options: typing.Dict[str, typing.Any] = { + "sniffamizer": "everywhere", + "sniff_ingredients": "vanilla_ingredients", + } + + def test_flags(self) -> None: + flags = self.world.options.flags + self.assertIn("S", flags) + self.assertNotIn("v", flags) + + +@skipUnless(hasattr(Sniffamizer, "option_everywhere"), "Feature disabled") +class TestSniffamizerEverywhereRandom(Bases.TestSniffamizerPool): + """ + Test that everywhere + random ingredients also adds extra items and locations + """ + options: typing.Dict[str, typing.Any] = { + "sniffamizer": "everywhere", + "sniff_ingredients": "random_ingredients", + } + + def test_flags(self) -> None: + flags = self.world.options.flags + self.assertIn("S", flags) + self.assertIn("v", flags) + + +@skipUnless(hasattr(Sniffamizer, "option_everywhere"), "Feature disabled") +class EverywhereAccessTest(SoETestBase): + """ + Test that everywhere has certain rules + """ + options: typing.Dict[str, typing.Any] = { + "sniffamizer": "everywhere", + } + + @staticmethod + def _resolve_numbers(spots: typing.Mapping[str, typing.Iterable[int]]) -> typing.List[str]: + return [f"{name} #{number}" for name, numbers in spots.items() for number in numbers] + + def test_knight_basher(self) -> None: + locations = ["Mungola", "Lightning Storm"] + self._resolve_numbers({ + "Gomi's Tower Sniff": range(473, 491), + "Gomi's Tower": range(195, 199), + }) + items = [["Knight Basher"]] + self.assertAccessDependency(locations, items)