Source code for pokemaster2.pokemon

"""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