412 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			412 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from __future__ import annotations
 | |
| import typing
 | |
| import random
 | |
| 
 | |
| 
 | |
| class AssembleOptions(type):
 | |
|     def __new__(mcs, name, bases, attrs):
 | |
|         options = attrs["options"] = {}
 | |
|         name_lookup = attrs["name_lookup"] = {}
 | |
|         # merge parent class options
 | |
|         for base in bases:
 | |
|             if getattr(base, "options", None):
 | |
|                 options.update(base.options)
 | |
|                 name_lookup.update(base.name_lookup)
 | |
|         new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
 | |
|                        name.startswith("option_")}
 | |
|         if "random" in new_options:
 | |
|             raise Exception("Choice option 'random' cannot be manually assigned.")
 | |
|         attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
 | |
|         options.update(new_options)
 | |
| 
 | |
|         # apply aliases, without name_lookup
 | |
|         options.update({name[6:].lower(): option_id for name, option_id in attrs.items() if
 | |
|                         name.startswith("alias_")})
 | |
| 
 | |
|         # auto-validate schema on __init__
 | |
|         if "schema" in attrs.keys():
 | |
|             def validate_decorator(func):
 | |
|                 def validate(self, *args, **kwargs):
 | |
|                     func(self, *args, **kwargs)
 | |
|                     self.value = self.schema.validate(self.value)
 | |
| 
 | |
|                 return validate
 | |
| 
 | |
|             attrs["__init__"] = validate_decorator(attrs["__init__"])
 | |
|         return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
 | |
| 
 | |
| 
 | |
| class Option(metaclass=AssembleOptions):
 | |
|     value: int
 | |
|     name_lookup: typing.Dict[int, str]
 | |
|     default = 0
 | |
| 
 | |
|     # convert option_name_long into Name Long as displayname, otherwise name_long is the result.
 | |
|     # Handled in get_option_name()
 | |
|     autodisplayname = False
 | |
| 
 | |
|     # can be weighted between selections
 | |
|     supports_weighting = True
 | |
| 
 | |
|     def __repr__(self) -> str:
 | |
|         return f"{self.__class__.__name__}({self.get_current_option_name()})"
 | |
| 
 | |
|     def __hash__(self):
 | |
|         return hash(self.value)
 | |
| 
 | |
|     @property
 | |
|     def current_key(self) -> str:
 | |
|         return self.name_lookup[self.value]
 | |
| 
 | |
|     def get_current_option_name(self) -> str:
 | |
|         """For display purposes."""
 | |
|         return self.get_option_name(self.value)
 | |
| 
 | |
|     @classmethod
 | |
|     def get_option_name(cls, value: typing.Any) -> str:
 | |
|         if cls.autodisplayname:
 | |
|             return cls.name_lookup[value].replace("_", " ").title()
 | |
|         else:
 | |
|             return cls.name_lookup[value]
 | |
| 
 | |
|     def __int__(self) -> int:
 | |
|         return self.value
 | |
| 
 | |
|     def __bool__(self) -> bool:
 | |
|         return bool(self.value)
 | |
| 
 | |
|     @classmethod
 | |
|     def from_any(cls, data: typing.Any):
 | |
|         raise NotImplementedError
 | |
| 
 | |
| 
 | |
| class Toggle(Option):
 | |
|     option_false = 0
 | |
|     option_true = 1
 | |
|     default = 0
 | |
| 
 | |
|     def __init__(self, value: int):
 | |
|         assert value == 0 or value == 1
 | |
|         self.value = value
 | |
| 
 | |
|     @classmethod
 | |
|     def from_text(cls, text: str) -> Toggle:
 | |
|         if text.lower() in {"off", "0", "false", "none", "null", "no"}:
 | |
|             return cls(0)
 | |
|         else:
 | |
|             return cls(1)
 | |
| 
 | |
|     @classmethod
 | |
|     def from_any(cls, data: typing.Any):
 | |
|         if type(data) == str:
 | |
|             return cls.from_text(data)
 | |
|         else:
 | |
|             return cls(data)
 | |
| 
 | |
|     def __eq__(self, other):
 | |
|         if isinstance(other, Toggle):
 | |
|             return self.value == other.value
 | |
|         else:
 | |
|             return self.value == other
 | |
| 
 | |
|     def __gt__(self, other):
 | |
|         if isinstance(other, Toggle):
 | |
|             return self.value > other.value
 | |
|         else:
 | |
|             return self.value > other
 | |
| 
 | |
|     def __bool__(self):
 | |
|         return bool(self.value)
 | |
| 
 | |
|     def __int__(self):
 | |
|         return int(self.value)
 | |
| 
 | |
|     @classmethod
 | |
|     def get_option_name(cls, value):
 | |
|         return ["No", "Yes"][int(value)]
 | |
| 
 | |
| 
 | |
| class DefaultOnToggle(Toggle):
 | |
|     default = 1
 | |
| 
 | |
| 
 | |
| class Choice(Option):
 | |
|     autodisplayname = True
 | |
| 
 | |
|     def __init__(self, value: int):
 | |
|         self.value: int = value
 | |
| 
 | |
|     @classmethod
 | |
|     def from_text(cls, text: str) -> Choice:
 | |
|         text = text.lower()
 | |
|         # TODO: turn on after most people have adjusted their yamls to no longer have suboptions with "random" in them
 | |
|         # maybe in 0.2?
 | |
|         # if text == "random":
 | |
|         #     return cls(random.choice(list(cls.options.values())))
 | |
|         for optionname, value in cls.options.items():
 | |
|             if optionname == text:
 | |
|                 return cls(value)
 | |
|         raise KeyError(
 | |
|             f'Could not find option "{text}" for "{cls.__name__}", '
 | |
|             f'known options are {", ".join(f"{option}" for option in cls.name_lookup.values())}')
 | |
| 
 | |
|     @classmethod
 | |
|     def from_any(cls, data: typing.Any) -> Choice:
 | |
|         if type(data) == int and data in cls.options.values():
 | |
|             return cls(data)
 | |
|         return cls.from_text(str(data))
 | |
| 
 | |
|     def __eq__(self, other):
 | |
|         if isinstance(other, self.__class__):
 | |
|             return other.value == self.value
 | |
|         elif isinstance(other, str):
 | |
|             assert other in self.options
 | |
|             return other == self.current_key
 | |
|         elif isinstance(other, int):
 | |
|             assert other in self.name_lookup
 | |
|             return other == self.value
 | |
|         elif isinstance(other, bool):
 | |
|             return other == bool(self.value)
 | |
|         else:
 | |
|             raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
 | |
| 
 | |
|     def __ne__(self, other):
 | |
|         if isinstance(other, self.__class__):
 | |
|             return other.value != self.value
 | |
|         elif isinstance(other, str):
 | |
|             assert other in self.options
 | |
|             return other != self.current_key
 | |
|         elif isinstance(other, int):
 | |
|             assert other in self.name_lookup
 | |
|             return other != self.value
 | |
|         elif isinstance(other, bool):
 | |
|             return other != bool(self.value)
 | |
|         else:
 | |
|             raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
 | |
| 
 | |
| 
 | |
| class Range(Option, int):
 | |
|     range_start = 0
 | |
|     range_end = 1
 | |
| 
 | |
|     def __init__(self, value: int):
 | |
|         if value < self.range_start:
 | |
|             raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__}")
 | |
|         elif value > self.range_end:
 | |
|             raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__}")
 | |
|         self.value = value
 | |
| 
 | |
|     @classmethod
 | |
|     def from_text(cls, text: str) -> Range:
 | |
|         text = text.lower()
 | |
|         if text.startswith("random"):
 | |
|             if text == "random-low":
 | |
|                 return cls(int(round(random.triangular(cls.range_start, cls.range_end, cls.range_start), 0)))
 | |
|             elif text == "random-high":
 | |
|                 return cls(int(round(random.triangular(cls.range_start, cls.range_end, cls.range_end), 0)))
 | |
|             elif text == "random-middle":
 | |
|                 return cls(int(round(random.triangular(cls.range_start, cls.range_end), 0)))
 | |
|             else:
 | |
|                 return cls(random.randint(cls.range_start, cls.range_end))
 | |
|         return cls(int(text))
 | |
| 
 | |
|     @classmethod
 | |
|     def from_any(cls, data: typing.Any) -> Range:
 | |
|         if type(data) == int:
 | |
|             return cls(data)
 | |
|         return cls.from_text(str(data))
 | |
| 
 | |
|     def get_option_name(self, value):
 | |
|         return str(self.value)
 | |
| 
 | |
|     def __str__(self):
 | |
|         return str(self.value)
 | |
| 
 | |
| 
 | |
| class OptionNameSet(Option):
 | |
|     default = frozenset()
 | |
| 
 | |
|     def __init__(self, value: typing.Set[str]):
 | |
|         self.value: typing.Set[str] = value
 | |
| 
 | |
|     @classmethod
 | |
|     def from_text(cls, text: str) -> OptionNameSet:
 | |
|         return cls({option.strip() for option in text.split(",")})
 | |
| 
 | |
|     @classmethod
 | |
|     def from_any(cls, data: typing.Any) -> OptionNameSet:
 | |
|         if type(data) == set:
 | |
|             return cls(data)
 | |
|         return cls.from_text(str(data))
 | |
| 
 | |
| 
 | |
| class OptionDict(Option):
 | |
|     default = {}
 | |
|     supports_weighting = False
 | |
|     value: typing.Dict[str, typing.Any]
 | |
| 
 | |
|     def __init__(self, value: typing.Dict[str, typing.Any]):
 | |
|         self.value = value
 | |
| 
 | |
|     @classmethod
 | |
|     def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
 | |
|         if type(data) == dict:
 | |
|             return cls(data)
 | |
|         else:
 | |
|             raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
 | |
| 
 | |
|     def get_option_name(self, value):
 | |
|         return ", ".join(f"{key}: {value}" for key, value in self.value.items())
 | |
| 
 | |
|     def __contains__(self, item):
 | |
|         return item in self.value
 | |
| 
 | |
| 
 | |
| class OptionList(Option):
 | |
|     default = []
 | |
|     supports_weighting = False
 | |
|     value: list
 | |
| 
 | |
|     def __init__(self, value: typing.List[str, typing.Any]):
 | |
|         self.value = value
 | |
|         super(OptionList, self).__init__()
 | |
| 
 | |
|     @classmethod
 | |
|     def from_text(cls, text: str):
 | |
|         return cls([option.strip() for option in text.split(",")])
 | |
| 
 | |
|     @classmethod
 | |
|     def from_any(cls, data: typing.Any):
 | |
|         if type(data) == list:
 | |
|             return cls(data)
 | |
|         return cls.from_text(str(data))
 | |
| 
 | |
|     def get_option_name(self, value):
 | |
|         return ", ".join(self.value)
 | |
| 
 | |
|     def __contains__(self, item):
 | |
|         return item in self.value
 | |
| 
 | |
| 
 | |
| class OptionSet(Option):
 | |
|     default = frozenset()
 | |
|     supports_weighting = False
 | |
|     value: set
 | |
| 
 | |
|     def __init__(self, value: typing.Union[typing.Set[str, typing.Any], typing.List[str, typing.Any]]):
 | |
|         self.value = set(value)
 | |
|         super(OptionSet, self).__init__()
 | |
| 
 | |
|     @classmethod
 | |
|     def from_text(cls, text: str):
 | |
|         return cls([option.strip() for option in text.split(",")])
 | |
| 
 | |
|     @classmethod
 | |
|     def from_any(cls, data: typing.Any):
 | |
|         if type(data) == list:
 | |
|             return cls(data)
 | |
|         elif type(data) == set:
 | |
|             return cls(data)
 | |
|         return cls.from_text(str(data))
 | |
| 
 | |
|     def get_option_name(self, value):
 | |
|         return ", ".join(self.value)
 | |
| 
 | |
|     def __contains__(self, item):
 | |
|         return item in self.value
 | |
| 
 | |
| 
 | |
| local_objective = Toggle  # local triforce pieces, local dungeon prizes etc.
 | |
| 
 | |
| 
 | |
| class Accessibility(Choice):
 | |
|     """Set rules for reachability of your items/locations.
 | |
|     Locations: ensure everything can be reached and acquired.
 | |
|     Items: ensure all logically relevant items can be acquired.
 | |
|     Minimal: ensure what is needed to reach your goal can be acquired."""
 | |
| 
 | |
|     option_locations = 0
 | |
|     option_items = 1
 | |
|     option_minimal = 2
 | |
|     alias_none = 2
 | |
|     default = 1
 | |
| 
 | |
| 
 | |
| class ProgressionBalancing(DefaultOnToggle):
 | |
|     """A system that moves progression earlier, to try and prevent the player from getting stuck and bored early."""
 | |
| 
 | |
| 
 | |
| common_options = {
 | |
|     "progression_balancing": ProgressionBalancing,
 | |
|     "accessibility": Accessibility
 | |
| }
 | |
| 
 | |
| 
 | |
| class ItemSet(OptionSet):
 | |
|     # implemented by Generate
 | |
|     verify_item_name = True
 | |
| 
 | |
| 
 | |
| class LocalItems(ItemSet):
 | |
|     """Forces these items to be in their native world."""
 | |
|     displayname = "Local Items"
 | |
| 
 | |
| 
 | |
| class NonLocalItems(ItemSet):
 | |
|     """Forces these items to be outside their native world."""
 | |
|     displayname = "Not Local Items"
 | |
| 
 | |
| 
 | |
| class StartInventory(OptionDict):
 | |
|     """Start with these items."""
 | |
|     verify_item_name = True
 | |
|     displayname = "Start Inventory"
 | |
| 
 | |
| 
 | |
| class StartHints(ItemSet):
 | |
|     """Start with these item's locations prefilled into the !hint command."""
 | |
|     displayname = "Start Hints"
 | |
| 
 | |
| 
 | |
| class ExcludeLocations(OptionSet):
 | |
|     """Prevent these locations from having an important item"""
 | |
|     displayname = "Excluded Locations"
 | |
|     verify_location_name = True
 | |
| 
 | |
| 
 | |
| per_game_common_options = {
 | |
|     # placeholder until they're actually implemented
 | |
|     "local_items": LocalItems,
 | |
|     "non_local_items": NonLocalItems,
 | |
|     "start_inventory": StartInventory,
 | |
|     "start_hints": StartHints,
 | |
|     "exclude_locations": OptionSet
 | |
| }
 | |
| 
 | |
| if __name__ == "__main__":
 | |
| 
 | |
|     from worlds.alttp.Options import Logic
 | |
|     import argparse
 | |
| 
 | |
|     map_shuffle = Toggle
 | |
|     compass_shuffle = Toggle
 | |
|     keyshuffle = Toggle
 | |
|     bigkey_shuffle = Toggle
 | |
|     hints = Toggle
 | |
|     test = argparse.Namespace()
 | |
|     test.logic = Logic.from_text("no_logic")
 | |
|     test.map_shuffle = map_shuffle.from_text("ON")
 | |
|     test.hints = hints.from_text('OFF')
 | |
|     try:
 | |
|         test.logic = Logic.from_text("overworld_glitches_typo")
 | |
|     except KeyError as e:
 | |
|         print(e)
 | |
|     try:
 | |
|         test.logic_owg = Logic.from_text("owg")
 | |
|     except KeyError as e:
 | |
|         print(e)
 | |
|     if test.map_shuffle:
 | |
|         print("map_shuffle is on")
 | |
|     print(f"Hints are {bool(test.hints)}")
 | |
|     print(test)
 | 
