core: new freetext and textchoice options (#728)

* add freetext and freetextchoice options

* fix textchoice. create plando_bosses bool so worlds can check if boss plando is enabled

* remove strange unneccessary \ escapes

* lttp: rip boss plando out of core

* fix broken text methods so they read the data correctly

* revert `None` key in boss_shuffle_options. fix failing tests

* lttp: rewrite boss plando

* lttp: rewrite boss shuffle

* add generic verification step and allow options to set a plando module

* add default typing to plando_options set

* use PlandoSettings intflag for lttp boss plando

* fix plandosettings boss flag check

* minor lttp init cleanup

* make suggested changes. account for "random" existing within plando boss options

* override eq operator

* Please document me!

* Forgot to mention it supports plando

* remove auto_display_name

* Throw warning alerting user to which shuffle is being used if plando is off. Set the remaining boss shuffle in init and boss placement cleanup

* move the convoluted string matching to `from_text`

* remove unneccessary text lowering and actually turn off plando option when it's disabled

* typing

* strong typing for verify method and reorder

* typing is your friend

* log warning correctly

* 3.8 support :(

* also list apparently

* rip out old boss shuffle spoiler code

* verification step for plando bosses and locations

* update plando guide to reference new supported behavior

* empty string is not `None`. remove unneccessary error throw

* Fix bad ordering

* validate boss_shuffle only contains a normal boss option at the end

* get random choice from a list dummy

* >:(

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* minor textchoice cleanup

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
This commit is contained in:
alwaysintreble
2022-09-16 19:55:33 -05:00
committed by GitHub
parent 8d51205e8f
commit 332dde154f
7 changed files with 323 additions and 131 deletions

View File

@@ -40,6 +40,17 @@ class AssembleOptions(abc.ABCMeta):
options.update(aliases)
if "verify" not in attrs:
# not overridden by class -> look up bases
verifiers = [f for f in (getattr(base, "verify", None) for base in bases) if f]
if len(verifiers) > 1: # verify multiple bases/mixins
def verify(self, *args, **kwargs) -> None:
for f in verifiers:
f(self, *args, **kwargs)
attrs["verify"] = verify
else:
assert verifiers, "class Option is supposed to implement def verify"
# auto-validate schema on __init__
if "schema" in attrs.keys():
@@ -117,6 +128,41 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
def from_any(cls, data: typing.Any) -> Option[T]:
raise NotImplementedError
if typing.TYPE_CHECKING:
from Generate import PlandoSettings
from worlds.AutoWorld import World
def verify(self, world: World, player_name: str, plando_options: PlandoSettings) -> None:
pass
else:
def verify(self, *args, **kwargs) -> None:
pass
class FreeText(Option):
"""Text option that allows users to enter strings.
Needs to be validated by the world or option definition."""
def __init__(self, value: str):
assert isinstance(value, str), "value of FreeText must be a string"
self.value = value
@property
def current_key(self) -> str:
return self.value
@classmethod
def from_text(cls, text: str) -> FreeText:
return cls(text)
@classmethod
def from_any(cls, data: typing.Any) -> FreeText:
return cls.from_text(str(data))
@classmethod
def get_option_name(cls, value: T) -> str:
return value
class NumericOption(Option[int], numbers.Integral):
# note: some of the `typing.Any`` here is a result of unresolved issue in python standards
@@ -373,6 +419,53 @@ class Choice(NumericOption):
__hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__
class TextChoice(Choice):
"""Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string"""
def __init__(self, value: typing.Union[str, int]):
assert isinstance(value, str) or isinstance(value, int), \
f"{value} is not a valid option for {self.__class__.__name__}"
self.value = value
super(TextChoice, self).__init__()
@property
def current_key(self) -> str:
if isinstance(self.value, str):
return self.value
else:
return self.name_lookup[self.value]
@classmethod
def from_text(cls, text: str) -> TextChoice:
if text.lower() == "random": # chooses a random defined option but won't use any free text options
return cls(random.choice(list(cls.name_lookup)))
for option_name, value in cls.options.items():
if option_name.lower() == text.lower():
return cls(value)
return cls(text)
@classmethod
def get_option_name(cls, value: T) -> str:
if isinstance(value, str):
return value
return cls.name_lookup[value]
def __eq__(self, other: typing.Any):
if isinstance(other, self.__class__):
return other.value == self.value
elif isinstance(other, str):
if other in self.options:
return other == self.current_key
return other == self.value
elif isinstance(other, int):
assert other in self.name_lookup, f"compared against an int that could never be equal. {self} == {other}"
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(NumericOption):
range_start = 0
range_end = 1
@@ -512,7 +605,7 @@ class VerifyKeys:
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
f"Allowed keys: {cls.valid_keys}.")
def verify(self, world):
def verify(self, world, player_name: str, plando_options) -> None:
if self.convert_name_groups and self.verify_item_name:
new_value = type(self.value)() # empty container of whatever value is
for item_name in self.value:
@@ -737,8 +830,8 @@ class ItemLinks(OptionList):
pool |= {item_name}
return pool
def verify(self, world):
super(ItemLinks, self).verify(world)
def verify(self, world, player_name: str, plando_options) -> None:
super(ItemLinks, self).verify(world, player_name, plando_options)
existing_links = set()
for link in self.value:
if link["name"] in existing_links: