"""Base Pokemon."""
import operator
from typing import Callable, Sequence, Type, TypeVar, Union
import attr
from pokemaster2.prng import PRNG
S = TypeVar("S", bound="Stats")
P = TypeVar("P", bound="BasePokemon")
STAT_NAMES = ["hp", "atk", "def_", "spatk", "spdef", "spd"]
STAT_NAMES_FULL = {
"hp": "hp",
"attack": "atk",
"defense": "def_",
"special-attack": "spatk",
"special-defense": "spdef",
"speed": "spd",
}
prng = PRNG()
[docs]@attr.s(auto_attribs=True)
class Stats:
"""Generic stats, can be used for Pokemon stats/IV/EV."""
hp: int
atk: int
def_: int
spatk: int
spdef: int
spd: int
def __add__(self: S, other: Union[S, int]) -> S:
"""Pointwise addition."""
return self._make_operator(operator.add, other)
def __sub__(self: S, other: Union[S, int]) -> S:
"""Pointwise subtraction."""
return self._make_operator(operator.sub, other)
def __mul__(self: S, other: Union[S, int]) -> S:
"""Pointwise multiplication."""
return self._make_operator(operator.mul, other)
def __floordiv__(self: S, other: Union[S, int]) -> S:
"""Pointwise floor division."""
return self._make_operator(operator.floordiv, other)
__radd__ = __add__
__rmul__ = __mul__
def _make_operator(
self: S,
operator: Callable[[int, int], int],
other: Union[S, int],
) -> S:
"""Programmatically create point-wise operators.
Args:
operator: A callable (Real, Real) -> Real.
other: If `other` is a `Stats` instance, then the
operator will be applied point-wisely. If `other` is a
number, then a scalar operation will be applied.
Raises:
TypeError: `other` should be either another `Stats` or `int`.
Returns:
A `Stats` instance.
"""
names = (
"hp",
"atk",
"def_",
"spatk",
"spdef",
"spd",
)
if not isinstance(other, type(self)) and not isinstance(other, int):
raise TypeError(
f"unsupported operand type(s) for {operator}: "
f"'{type(self)}' and '{type(other)}'"
)
result_stats = {}
for stat in names:
if isinstance(other, type(self)):
result_stats[stat] = int(operator(getattr(self, stat), getattr(other, stat)))
elif isinstance(other, int):
result_stats[stat] = int(operator(getattr(self, stat), other))
return self.__class__(**result_stats)
[docs] def validate_iv(self: S) -> bool:
"""Check if each IV is between 0 and 32."""
for stat in STAT_NAMES:
if not 0 <= getattr(self, stat) <= 32:
raise ValueError(
f"The {stat} IV ({getattr(self, stat)}) must be a number "
"between 0 and 32 inclusive."
)
return True
[docs] @classmethod
def create_iv(cls: Type[S], gene: int) -> S:
"""Create IV stats from a Pokémon's gene.
Args:
gene: An `int` generated by the PRNG.
Returns:
A `Stats` instance.
"""
return cls(
hp=gene % 32,
atk=(gene >> 5) % 32,
def_=(gene >> 10) % 32,
spd=(gene >> 16) % 32,
spatk=(gene >> 21) % 32,
spdef=(gene >> 26) % 32,
)
[docs] @classmethod
def zeros(cls: Type[S]) -> S:
"""Empty Stats."""
return cls(
hp=0,
atk=0,
def_=0,
spatk=0,
spdef=0,
spd=0,
)
[docs] @classmethod
def nature_modifiers(cls: Type[S], nature: str) -> S:
"""Generate nature modifier Stats."""
# nature_data = _db.get_nature(identifier=nature)
# modifiers = {}
# for stat in STAT_NAMES:
# modifiers[stat] = 1
# if nature_data.is_neutral:
# return cls(**modifiers)
# modifiers[STAT_NAMES_FULL[nature_data.increased_stat.identifier]] = 1.1
# modifiers[STAT_NAMES_FULL[nature_data.decreased_stat.identifier]] = 0.9
# return cls(**modifiers)
[docs]@attr.s(auto_attribs=True)
class BasePokemon:
"""The underlying structure of a Pokémon.
No fancy initializations, no consistency checks, just a very basic
Pokémon model. Anything is possible with this BasePokemon. This
class also contains common and basic behaviors of Pokémon, such as
leveling-up, learning/forgetting moves, evolving into another
Pokémon, etc.
This class is never meant to be instantiated directly.
"""
national_id: int
species: str
types: Sequence[str]
item_held: str
exp: int
level: int
base_stats: Stats
iv: Stats
current_stats: Stats
stats: Stats
ev: Stats
# move_set = Mapping[int, Mapping[str, Union[str, int]]]
pid: str
gender: str
nature: str
ability: str
# def evolve(self: P) -> None:
# """
# Evolve into another Pokémon.
# 1. Statistics are updated.
# 2. Learnset is updated.
# 3. Evolution tree is updated.
# Returns:
# Nothing
# """
# pass
# def level_up(self: P) -> None:
# """Increase `Pokemon`'s level by 1.
# Returns:
# Nothing
# """
# pass
# @classmethod
# def _from_pokedex_by_id(
# cls: "BasePokemon",
# national_id: int,
# level: int,
# item_held: str = None,
# iv: Stats = None,
# ev: Stats = None,
# pid: int = None,
# nature: str = None,
# ability: str = None,
# gender: str = None,
# ) -> "BasePokemon":
# """Instantiate a `BasePokemon` by its national id.
# Everything else is randomized.
# Args:
# national_id: the Pokemon's ID in the National Pokedex.
# level: Pokemon's level.
# item_held: Pokemon's holding item.
# iv: Pokemon's individual values, `Stats`, used to determine its permanent stats.
# A random IV will be set if not provided.
# ev: Pokemon's effort values, `Stats`, used to determine its permanent stats. An
# all-zero ev will be set if not provided.
# pid: Pokemon's personality id. `nature`, `ability`, and `gender` will use
# their provided value first. A random `pid` will be set if not provided.
# nature: Pokemon's nature, used to determine its permanent stats. If nothing is
# provided, then the function will use `pid` to determine its `nature`.
# ability: Pokemon's ability, `str`. If nothing is provided, then the function
# will use `pid` to determine its `nature`.
# gender: Pokemon's gender. If nothing is provided, then the function will use
# `pid` to determine its `nature`.
# Returns:
# A `BasePokemon` instance.
# """
# # Build pokemon data
# pokemon_data = _db.get_pokemon(national_id=national_id)
# growth_data = _db.get_experience(national_id=national_id, level=level)
# species_data = pokemon_data.species
# species = species_data.identifier
# # Determine stats
# gene = prng.create_gene()
# iv = iv or Stats.create_iv(gene=gene)
# ev = ev or Stats.zeros()
# base_stats = {}
# for i, stat in enumerate(STAT_NAMES):
# base_stats[stat] = pokemon_data.stats[i].base_stat
# stats = _calc_stats(level=level, base_stats=base_stats, iv=iv, ev=ev, nature=nature)
# current_stats = stats
# # PID related attributes
# pid = pid or prng.create_personality()
# nature = nature or _db.get_nature(pid).identifier
# ability = ability or _db.get_ability(species=species, personality=pid).identifier
# gender = gender or _db.get_pokemon_gender(species=species, personality=pid).identifier
# return cls(
# pid=pid,
# national_id=species_data.id,
# species=species,
# types=list(map(lambda x: x.identifier, pokemon_data.types)),
# item_held=item_held,
# exp=growth_data.experience,
# level=growth_data.level,
# stats=stats,
# current_stats=current_stats,
# ev=ev,
# iv=iv,
# nature=nature,
# ability=ability,
# gender=gender,
# )
def _calc_stats(level: int, base_stats: Stats, iv: Stats, ev: Stats, nature: str) -> Stats:
"""Calculate the Pokemon's stats."""
nature_modifiers = Stats.nature_modifiers(nature)
residual_stats = Stats(
hp=10 + level,
atk=5,
def_=5,
spatk=5,
spdef=5,
spd=5,
)
stats = ((base_stats * 2 + iv + ev // 4) * level // 100 + residual_stats) * nature_modifiers
if base_stats.hp == 1:
stats.hp = 1
return stats