from gd.crypto import Key, decode_robtop_str, encode_robtop_str
from gd.enums import DemonDifficulty, LevelDifficulty
from gd.text_utils import make_repr
from gd.typing import Any, Dict, Optional, Tuple, Union
__all__ = (
"GameVersion",
"Password",
"Version",
"get_actual_difficulty",
)
class Version:
"""Class that represents version of anything.
.. container:: operations
.. describe:: x == y
Check if two versions are equal.
.. describe:: x != y
Check if two versions are not equal.
.. describe:: x > y
Check if one version is strictly greater than another.
.. describe:: x >= y
Check if one version is greater than or equal to another.
.. describe:: x < y
Check if one version is strictly lower than another.
.. describe:: x <= y
Check if one version is lower than or equal to another.
.. describe:: str(x)
Return short representation of the version; for ``Version(x, y)``, return ``x.y``.
.. describe:: repr(x)
Return representation of the version, useful for debugging.
Parameters
----------
major: :class:`int`
Major part of the version.
minor: :class:`int`
Minor part of the version.
"""
def __init__(self, major: int, minor: int) -> None:
self._major = major
self._minor = minor
def __json__(self) -> str:
return str(self)
@property
def major(self) -> int:
""":class:`int`: Major part of the version."""
return self._major
@property
def minor(self) -> int:
""":class:`int`: Minor part of the version."""
return self._minor
@property
def parts(self) -> Tuple[int, int]:
"""Tuple[:class:`int`, :class:`int`]: All parts of the version, as tuple object."""
return (self._major, self._minor)
def __repr__(self) -> str:
info = {"major": self.major, "minor": self.minor}
return make_repr(self, info)
def __str__(self) -> str:
return f"{self.major}.{self.minor}"
def __eq__(self, other: Any) -> bool:
if not isinstance(other, self.__class__):
return NotImplemented
return (self.major, self.minor) == (other.major, other.minor)
def __ne__(self, other: Any) -> bool:
if not isinstance(other, self.__class__):
return NotImplemented
return (self.major, self.minor) != (other.major, other.minor)
def __lt__(self, other: Any) -> bool:
if not isinstance(other, self.__class__):
return NotImplemented
return (self.major, self.minor) < (other.major, other.minor)
def __gt__(self, other: Any) -> bool:
if not isinstance(other, self.__class__):
return NotImplemented
return (self.major, self.minor) > (other.major, other.minor)
def __le__(self, other: Any) -> bool:
return self < other or self == other
def __ge__(self, other: Any) -> bool:
return self > other or self == other
@classmethod
def from_number(cls, number: int) -> "Version":
"""Create a version from number, e.g. ``21`` -> ``2.1``."""
major, minor = divmod(number, 10)
return cls(major, minor)
def to_number(self) -> int:
"""Convert the version to a number, e.g. ``1.9`` -> ``19``."""
return self.major * 10 + self.minor
class GameVersion(Version):
"""Derived from :class:`~gd.Version`, contains additional features to convert game version."""
@classmethod
def from_robtop_number(cls, number: int) -> "GameVersion":
"""Convert RobTop's ``number`` to :class:`~gd.GameVersion`."""
if 0 < number < 8:
return cls(1, number - 1)
elif number == 10:
return cls(1, 7)
elif number < 10:
raise ValueError(f"Invalid version provided: {number}.")
major, minor = divmod(number, 10)
return cls(major, minor)
def to_robtop_number(self) -> int:
"""Convert :class:`~gd.GameVersion` to a RobTop's number."""
major = self._major
minor = self._minor
if major == 1:
if minor == 7:
return 10
elif minor < 7:
return minor + 1
return major * 10 + minor
@classmethod
def from_robtop(cls, string: str) -> "GameVersion":
"""Same as :meth:`~gd.GameVersion.from_robtop_number`, but operates on ``string``."""
return cls.from_robtop_number(int(string))
def to_robtop(self) -> str:
"""Same as :meth:`~gd.GameVersion.to_robtop_number`, but returns a string."""
return str(self.to_robtop_number())
[docs]class Password:
"""Class that represents passwords.
.. container:: operations
.. describe:: str(x)
Return a human-friendly description of the password.
For example, ``copyable, password 101010``.
.. describe:: repr(x)
Return representation of the password, useful for debugging.
Parameters
----------
password: Optional[Union[:class:`int`, :class:`str`]]
Actual password, as a number or a string.
copyable: :class:`bool`
Whether passwords implies that something is copyable.
If ``True`` and ``password`` is ``None``, free copy is assumed.
"""
_ADD = 1_000_000
def __init__(self, password: Optional[Union[int, str]] = None, copyable: bool = True) -> None:
if password is None:
self._password = None
else:
self._password = int(password)
if self._password >= self._ADD:
raise ValueError(f"Password is too large: {password}.")
self._copyable = bool(copyable)
def __json__(self) -> Dict[str, Union[Optional[int], bool]]:
return {"password": self.password, "copyable": self.copyable}
def __str__(self) -> str:
if self.copyable:
if self.password is None:
return "copyable, no password"
else:
return f"copyable, password {self.password}"
return "not copyable"
def __repr__(self) -> str:
info = {"password": self.password, "copyable": self.copyable}
return make_repr(self, info)
@property
def copyable(self) -> bool:
""":class:`bool`: Whether password implies that something is copyable."""
return self._copyable
@property
def password(self) -> Optional[int]:
"""Optional[:class:`int`]: Actual password, ``None`` if not given."""
return self._password
[docs] @classmethod
def from_robtop_number(cls, number: int) -> "Password":
"""Create :class:`~gd.Password` from ``number``."""
if number == 0:
return cls(None, False)
if number == 1:
return cls(None, True)
return cls(number % cls._ADD, True)
[docs] def to_robtop_number(self) -> int:
"""Convert :class:`~gd.Password` to a number."""
if self._copyable:
if self._password is None:
return 1
else:
return self._password + self._ADD
else:
return 0
[docs] @classmethod
def from_robtop(cls, string: str) -> "Password":
"""Same as :meth:`~gd.Password.from_robtop_number`, except it attempts decoding."""
try:
password = decode_robtop_str(string, Key.LEVEL_PASSWORD) # type: ignore
except Exception: # noqa
password = string
if password.isdigit():
return cls.from_robtop_number(int(password))
else:
return cls(None, False)
[docs] def to_robtop(self, encode: bool = True) -> str:
"""Same as :meth:`~gd.Password.to_robtop_number`, except it optionally applies encoding."""
number = self.to_robtop_number()
if not number or not encode:
return str(number)
else:
return encode_robtop_str(str(number), Key.LEVEL_PASSWORD) # type: ignore
VALUE_TO_LEVEL_DIFFICULTY = {
0: LevelDifficulty.NA,
1: LevelDifficulty.EASY,
2: LevelDifficulty.NORMAL,
3: LevelDifficulty.HARD,
4: LevelDifficulty.HARDER,
5: LevelDifficulty.INSANE,
6: LevelDifficulty.DEMON,
}
VALUE_TO_DEMON_DIFFICULTY = {
0: DemonDifficulty.HARD_DEMON,
3: DemonDifficulty.EASY_DEMON,
4: DemonDifficulty.MEDIUM_DEMON,
5: DemonDifficulty.INSANE_DEMON,
6: DemonDifficulty.EXTREME_DEMON,
}
def value_to_level_difficulty(value: int) -> LevelDifficulty:
return VALUE_TO_LEVEL_DIFFICULTY.get(value, LevelDifficulty.NA) # type: ignore
def value_to_demon_difficulty(value: int) -> DemonDifficulty:
return VALUE_TO_DEMON_DIFFICULTY.get(value, DemonDifficulty.HARD_DEMON) # type: ignore
[docs]def get_actual_difficulty(
level_difficulty: int, demon_difficulty: int, is_auto: bool, is_demon: bool
) -> Union[LevelDifficulty, DemonDifficulty]:
"""Get real level difficulty from given parameters.
Parameters
----------
level_difficulty: :class:`int`
Number that represents level difficulty.
demon_difficulty: :class:`int`
Number that represents level demon difficulty.
is_auto: :class:`bool`
Whether the level is auto.
is_demon: :class:`bool`
Whether the level is demon.
Returns
-------
Union[:class:`~gd.LevelDifficulty`, :class:`~gd.DemonDifficulty`]
Level or demon difficulty, based on parameters.
"""
if is_auto:
return LevelDifficulty.AUTO # type: ignore
elif is_demon:
return value_to_demon_difficulty(demon_difficulty)
return value_to_level_difficulty(level_difficulty)