"""Common utilities used across the package"""
from __future__ import print_function, division
import collections
import hashlib
import json
import re
import pkg_resources
from past.builtins import basestring
from onix.model import Moveset, Forme, PokeStats
[docs]def sanitize_string(input_string):
"""
Strips all non-alphanumeric characters and puts everything in lowercase
Args:
input_string (str) : string to be sanitized
Returns:
str : the sanitized string
Examples:
>>> from onix.utilities import sanitize_string
>>> print(sanitize_string('Hello World'))
helloworld
"""
return re.compile(r'[^A-Za-z0-9]').sub('', input_string).lower()
[docs]class Sanitizer(object):
"""
An object which normalizes inputs to ensure consistency, removing or
replacing invalid characters and de-aliasing
Args:
pokedex (dict) : the Pokedex to use, scraped from Pokemon Showdown
aliases (dict) : the alias lookup to use, scraped from Pokemon
Showdown
"""
def __init__(self, pokedex, aliases):
self.aliases = aliases.copy()
for pokemon in pokedex.keys():
if 'otherForms' in pokedex[pokemon].keys():
for form in pokedex[pokemon]['otherForms']:
self.aliases[sanitize_string(form)] = pokemon
[docs] def sanitize(self, input_object):
"""
Sanitizes the given object, sorting it, removing or replacing invalid
characters, and de-aliasing, as required
Args:
input_object (object) : the object to sanitize
Returns:
object : the sanitized object, of the same type as the input
Raises:
TypeError : if the type of the `input_object` is not supported
Examples:
>>> import json
>>> from onix import scrapers
>>> from onix import utilities
>>> try:
... pokedex = json.load(open('.psdata/pokedex.json'))
... except IOError:
... pokedex = scrapers.scrape_battle_pokedex()
...
>>> try:
... aliases = json.load(open('.psdata/aliases.json'))
... except IOError:
... aliases = scrapers.scrape_battle_aliases()
...
>>> sanitizer = utilities.Sanitizer(pokedex, aliases)
>>> sanitizer.sanitize('Wormadam-Trash')
'wormadamtrash'
>>> print(', '.join(sanitizer.sanitize(['Volt Switch', 'Thunder',
... 'Giga Drain', 'Web'])))
gigadrain, stickyweb, thunder, voltswitch
"""
if input_object is None:
sanitized = input_object
elif isinstance(input_object, basestring):
sanitized = sanitize_string(input_object)
if sanitized in self.aliases.keys():
sanitized = sanitize_string(self.aliases[sanitized])
elif isinstance(input_object, Moveset):
sanitized_dict = self.sanitize(dict(zip(input_object._fields[1:],
input_object[1:])))
primary_forme = self.sanitize(input_object.formes[0])
alternate_formes = self.sanitize(input_object.formes[1:])
sanitized_dict['formes'] = [primary_forme] + alternate_formes
sanitized = Moveset(**sanitized_dict)
elif isinstance(input_object, Forme):
sanitized_dict = self.sanitize(dict(zip(input_object._fields,
input_object)))
sanitized = Forme(**sanitized_dict)
elif isinstance(input_object, dict):
sanitized = dict()
for key in input_object.keys():
try:
sanitized[key] = self.sanitize(input_object[key])
except TypeError: # if the value can't be sanitized, leave it
sanitized[key] = input_object[key]
elif isinstance(input_object, collections.Iterable):
sanitized = sorted([self.sanitize(item) for item in input_object])
else:
raise TypeError("Sanitizer: cannot sanitize {0}"
.format(type(input_object)))
return sanitized
[docs]def compute_sid(moveset, sanitizer=None):
"""
Computes the Set ID for the given moveset
Args:
moveset (Moveset): the moveset to compute the SID for
sanitizer (:obj:`Sanitizer`, optional): if no sanitizer is provided,
``moveset`` is assumed to be already sanitized. Otherwise, the
provided ``Sanitizer`` is used to sanitize the moveset.
Returns:
str : the corresponding Set ID
Examples:
>>> from onix.model import PokeStats, Forme, Moveset
>>> from onix import utilities
>>> moveset = Moveset([Forme('mamoswine','thickfat',
... PokeStats(361,394,197,158,156,259))],
... 'f', 'lifeorb',
... ['earthquake', 'iceshard', 'iciclecrash',
... 'superpower'], 100, 255)
>>> print(utilities.compute_sid(moveset)) #doctest: +ELLIPSIS
ad9a9fa20...
"""
if sanitizer is not None:
moveset = sanitizer.sanitize(moveset)
moveset_hash = hashlib.sha512(repr(moveset).encode('utf-8')).hexdigest()
# may eventually want to truncate hash, e.g.
# moveset_hash = moveset_hash[:16]
return moveset_hash
[docs]def stats_dict_to_model(stats_dict):
"""
Converts a Pokemon Showdown-style stats ``dict`` to a ``PokeStats`` object.
Args:
stats_dict (dict) : the object to convert
Returns:
PokeStats : the converted object
Raises:
TypeError : if ``stats_dict`` doesn't have the correct keys
Examples:
>>> from onix import utilities
>>> utilities.stats_dict_to_model({'hp': 361, 'atk': 394, 'def': 197,
... 'spa': 158, 'spd' : 156, 'spe': 259})
PokeStats(hp=361, atk=394, dfn=197, spa=158, spd=156, spe=259)
"""
stats_dict = stats_dict.copy()
stats_dict['dfn'] = stats_dict.pop('def')
return PokeStats(**stats_dict)
[docs]def calculate_stats(base_stats, nature, ivs, evs, level):
"""
Calculate a Pokemon's battle stats
Args:
base_stats (PokeStats) : the Pokemon's base stats
nature (dict) : the nature, with ``plus`` and ``minus`` keys indicating
the stats that are boosted and hindered (neutral natures will have
neither key)
ivs (PokeStats) : the Pokemon's individual values
evs (PokeStats) : the Pokemon's effort values
level (int) : the Pokemon's level
Returns:
PokeStats : the Pokemon's battle stats
Examples:
>>> from onix.model import PokeStats
>>> from onix import utilities
>>> utilities.calculate_stats(PokeStats(108, 130, 95, 80, 85, 102),
... {'name': 'Adamant', 'plus': 'atk', 'minus': 'spa'},
... PokeStats(24, 12, 30, 16, 23, 5),
... PokeStats(74, 195, 86, 48, 84, 23), 78)
PokeStats(hp=289, atk=279, dfn=192, spa=135, spd=171, spe=171)
"""
stats = dict()
if base_stats.hp == 1: # Shedinja
stats['hp'] = 1
else:
stats['hp'] = (base_stats.hp * 2 + ivs.hp + evs.hp // 4 + 100)\
* level // 100 + 10
for stat in ('atk', 'dfn', 'spa', 'spd', 'spe'):
stats[stat] = (getattr(base_stats, stat) * 2 + getattr(ivs, stat) +
getattr(evs, stat) // 4) * level // 100 + 5
if 'plus' in nature.keys() or 'minus' in nature.keys():
# we want it to throw an error if the nature is invalid
stats[nature['plus']] = int(stats[nature['plus']]*1.1)
stats[nature['minus']] = int(stats[nature['minus']] * 0.9)
return PokeStats(**stats)
[docs]def load_natures():
"""
Loads the natures dictionary
Returns:
dict : the natures dictionary
Examples:
>>> from onix import utilities
>>> natures = utilities.load_natures()
>>> print(natures['mild']['minus'])
dfn
"""
json_string = pkg_resources.resource_string('onix.resources',
'natures.json').decode('utf-8')
return json.loads(json_string)
[docs]def load_species_lookup():
"""
Loads the dictionary of sanitized formes (and forme-concatenations) to their
display names. This is what handles things like determining whether megas
are tiered together or separately or what counts as an "appearance-only"
forme.
Returns:
dict : the species lookup dictionary
Examples:
>>> from onix import utilities
>>> species_lookup = utilities.load_species_lookup()
>>> print(species_lookup['darmanitanzen'])
Zen-Darmanitan
"""
json_string = pkg_resources.resource_string('onix.resources',
'species_lookup.json') \
.decode('utf-8')
return json.loads(json_string)
[docs]def parse_ruleset(ruleset):
"""
Extract information from a ruleset dict (an entry from `formats.json`)
that's relevant to log reading / stat summing, etc.
Args:
ruleset (dict): the entry from `formats.json` corresponding to the
format of interest
Returns:
tuple :
- (str) : what's the game type? That is, `'singles'` vs. `'doubles'`
vs. whatever
- (bool) : is it a Hackmons metatame?
- (bool) : are illegal species / ability combos allowed?
- (bool) :
is Rayquaza allowed to mega-evolve in the metagame?
.. note::
If Rayquaza is banned from the metagame, this is trivial
(and will probably return True)
- (int) : the default level of a Pokemon in the metagame
Examples:
>>> import json
>>> from onix import scrapers
>>> from onix import utilities
>>> commit = '5c14138b54dddf8bc034433eaef950a1c6eaf734'
>>> try:
... formats = json.load(
... open('.psdata/{}/formats.json'.format(commit)))
... except IOError:
... formats = scrapers.scrape_battle_formats(commit=commit)
>>> print(utilities.parse_ruleset(formats['nu']))
('singles', False, False, True, 100)
>>> print(utilities.parse_ruleset(formats['almostanyability']))
('singles', False, True, True, 100)
>>> print(utilities.parse_ruleset(formats['doublesuu']))
('doubles', False, False, True, 100)
"""
# defaults
game_type = 'singles'
hackmons = True
any_ability = False
mega_rayquaza_allowed = True
default_level = 100
if 'gameType' in ruleset.keys():
game_type = str(ruleset['gameType'])
if len({'Standard', 'Standard Doubles', 'Standard GBU', 'Standard NEXT',
'Standard Ubers'}.intersection(ruleset['ruleset'])) > 0:
hackmons = False
if 'banlist' in ruleset.keys():
if 'Illegal' in ruleset['banlist']:
hackmons = False
if 'Ignore Illegal Abilities' in ruleset['banlist']:
any_ability = True
if 'Mega Rayquaza Clause' in ruleset['ruleset']:
mega_rayquaza_allowed = False
if hackmons:
any_ability = True
if 'maxLevel' in ruleset.keys():
default_level = ruleset['maxLevel']
elif 'maxForcedLevel' in ruleset.keys():
default_level = ruleset['maxForcedLevel']
return (game_type, hackmons, any_ability, mega_rayquaza_allowed,
default_level)
[docs]def determine_hidden_power_type(ivs):
"""
Determine a Pokemon's Hidden Power type from its IVs. One should never
need to do this (PS automatically detemines the type and puts it in the
moveset), but it's best to be sure
Args:
ivs (PokeStats) : The Pokemon's individual values
Returns:
str : hidden power type
Examples:
>>> from onix import utilities as utl
>>> from onix.model import PokeStats
>>> utl.determine_hidden_power_type(PokeStats(31, 31, 31, 31, 31, 31))
'dark'
>>> utl.determine_hidden_power_type(PokeStats(31, 0, 30, 31, 31, 31))
'ice'
"""
type_index = 15 * (ivs.hp % 2 + 2 * (ivs.atk % 2) + 4 * (ivs.dfn % 2) + 8 *
(ivs.spe % 2) + 16 * (ivs.spa % 2)
+ 32 * (ivs.spd % 2)) // 63
return ['fighting', 'flying', 'poison', 'ground', 'rock', 'bug', 'ghost',
'steel', 'fire', 'water', 'grass', 'electric', 'psychic', 'ice',
'dragon', 'dark'][type_index]