Archipelago/Options.py

282 lines
8.7 KiB
Python
Raw Normal View History

from __future__ import annotations
import typing
2021-06-08 12:15:23 +00:00
import random
class AssembleOptions(type):
def __new__(mcs, name, bases, attrs):
options = attrs["options"] = {}
name_lookup = attrs["name_lookup"] = {}
2021-08-03 17:03:41 +00:00
# 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
2021-03-20 23:47:17 +00:00
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)
2021-03-20 23:47:17 +00:00
# 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)
2021-08-03 17:03:41 +00:00
class Option(metaclass=AssembleOptions):
value: int
name_lookup: typing.Dict[int, str]
default = 0
2021-08-03 17:03:41 +00:00
# convert option_name_long into Name Long as displayname, otherwise name_long is the result.
# Handled in get_option_name()
autodisplayname = False
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)
2021-08-31 20:14:18 +00:00
@classmethod
def get_option_name(cls, value: typing.Any) -> str:
if cls.autodisplayname:
return cls.name_lookup[value].replace("_", " ").title()
2021-08-03 17:03:41 +00:00
else:
2021-08-31 20:14:18 +00:00
return cls.name_lookup[value]
2021-08-03 17:03:41 +00:00
def __int__(self) -> int:
return self.value
2021-08-03 17:03:41 +00:00
def __bool__(self) -> bool:
return bool(self.value)
2021-03-14 07:38:02 +00:00
@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):
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)
2021-03-14 07:38:02 +00:00
@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
2021-03-20 23:47:17 +00:00
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
2021-08-03 17:03:41 +00:00
class Choice(Option):
2021-08-03 17:03:41 +00:00
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())}')
2021-03-14 07:38:02 +00:00
@classmethod
2021-05-09 15:46:26 +00:00
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))
2021-03-14 07:38:02 +00:00
def __eq__(self, other):
if 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, 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__}")
2021-06-08 13:39:34 +00:00
class Range(Option, int):
2021-06-08 12:15:23 +00:00
range_start = 0
range_end = 1
2021-06-08 13:39:34 +00:00
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
2021-06-08 12:15:23 +00:00
@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)))
2021-06-12 19:05:45 +00:00
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))
2021-06-08 13:39:34 +00:00
return cls(int(text))
2021-06-08 12:15:23 +00:00
@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)
2021-06-08 13:39:34 +00:00
def __str__(self):
return str(self.value)
2021-06-08 13:39:34 +00:00
2021-05-09 15:46:26 +00:00
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 = {}
def __init__(self, value: typing.Dict[str, typing.Any]):
self.value: typing.Dict[str, typing.Any] = 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 str(value)
2021-05-09 15:46:26 +00:00
2021-06-08 13:39:34 +00:00
2021-03-20 23:47:17 +00:00
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
class Accessibility(Choice):
option_locations = 0
option_items = 1
option_beatable = 2
if __name__ == "__main__":
from worlds.alttp.Options import Logic
import argparse
2021-08-30 16:00:39 +00:00
map_shuffle = Toggle
compass_shuffle = Toggle
keyshuffle = Toggle
2021-08-30 16:00:39 +00:00
bigkey_shuffle = Toggle
hints = Toggle
test = argparse.Namespace()
test.logic = Logic.from_text("no_logic")
2021-08-30 16:00:39 +00:00
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)
2021-08-30 16:00:39 +00:00
if test.map_shuffle:
print("map_shuffle is on")
print(f"Hints are {bool(test.hints)}")
print(test)