Example usage

pokehelpyer is a Python package designed to assist Pokémon players in building teams of pokémon. Users can provide a list of pokémon currently on their team, and pokehelpyer will make a suitable recommendation for pokémon which can strengthen the current team by balancing it’s overall weaknesses and resistances.

Given a list of up to five pokémon currently on a team, pokehelpyer will determine which types the team is most weak to and which types the team is most resistant to. Based on this information, pokehelpyer will then recommend a pokémon that could be added to the current team to make its weaknesses and resistances more balanced.

To use pokehelpyer in a project, we will first import the package as shown below:

import pokehelpyer.pokehelpyer as pk

pokehelpyer currently includes five different functions which can be used to explore the resistances and weaknesses of our pokémon team. Using these functions effectively can help us build a strong team of pokémon.

get_types

The get_types function takes an input list of pokémon currently in our team and determines the type of each pokémon using an existing dataset. Please note that some pokémon have dual types and some pokémon have a single type. Hence, the output will be a list of list of strings in which each inner list will contain the type(s) of the pokémon.

In the example below, Pikachu, Eevee, and Metapod are all single-type pokémon, whereas Charizard is a dual-type pokémon.

team_list = ['Pikachu', 'Eevee', 'Charizard', 'Metapod']
team_types = pk.get_types(team_list)

team_types
[['Electric'], ['Normal'], ['Fire', 'Flying'], ['Bug']]

calc_weaknesses and calc_resistances

Each type of pokémon is weak to and resistant to attacks from certain other types of pokémon. For example, fire-type pokémon are weak to water-type attacks and resistant to bug-type attacks.

This is complicated a bit by dual-types pokémon. Some dual-typed pokemon can be doubly weak or doubly resistant to certain types. For an example of double weakness, we have Abomasnow, which is both ice-type and grass-type, and as a result is doubly weak to fire-type attacks. An example of double resistance is Skarmory, which is both steel-type and flying-type, and as a result is doubly resistant to bug-type attacks and grass-type attacks.

Further, some dual-typed pokemon can have a weakness cancel out with a resistance. For an example, we can consider Skarmory yet again. Flying-type pokemon are weak to rock-type and ice-type attacks, but steel-type pokemon are resistant to rock-type and ice-type attacks. As a result, Skarmory takes normal damage from rock-type and ice-type attacks.

The input for calc_weaknesses / calc_resistances should be a list of lists strings which contains the types of the pokémon on our team (i.e. the output from get_types). These functions then calculate a measure of how weak / resistant our team is to each pokemon type in the game. The output is a dictionary whose keys are each of the 18 types in the game and whose values are integers measuring the level of weakness / resistance our team has to each type. Higher values indicate a higher level of weakness / resistance to that type (key).

team_weaknesses = pk.calc_weaknesses(team_types)
team_resistances = pk.calc_resistances(team_types)

print(f'weaknesses: {team_weaknesses}\n')
print(f'resistances: {team_resistances}\n')
weaknesses: {'Normal': 0, 'Fire': 1, 'Water': 1, 'Electric': 1, 'Grass': 0, 'Ice': 0, 'Fighting': 1, 'Poison': 0, 'Ground': 1, 'Flying': 1, 'Psychic': 0, 'Bug': 0, 'Rock': 3, 'Ghost': 0, 'Dragon': 0, 'Dark': 0, 'Steel': 0, 'Fairy': 0}

resistances: {'Normal': 0, 'Fire': 1, 'Water': 0, 'Electric': 1, 'Grass': 3, 'Ice': 0, 'Fighting': 2, 'Poison': 0, 'Ground': 4, 'Flying': 1, 'Psychic': 0, 'Bug': 2, 'Rock': 0, 'Ghost': 3, 'Dragon': 0, 'Dark': 0, 'Steel': 2, 'Fairy': 1}

calc_balance

Since the recommend function uses the information in the dictionaries created by calc_weaknesses and calc_resistances to determine which pokemon could be added to the existing team to make it more “balanced”, we must first define a metric to measure how “balanced” a team is. To this end, we use the following:

For $i=1,2,\dots, 18$, let $w_i$ be the value corresponding to the $i$ -th pokemon type in the dictionary returned by calc_weaknesses and let $r_i$ be the value corresponding to the $i$ -th pokemon type in the dictionary returned by calc_resistances. Now define $\delta_i = r_i - w_i$. If $\delta_i < 0$, then the team is weak (overall) to pokemon type $i$, and this should definitely be considered. The more negative $\delta_i$ is, the more problematic. For example, if a player used a team of 5 fire type pokemon, then $\delta_{\text{water}}$ would be equal to -5, and this would be a big problem. If $\delta_i > 0$, then the team is resistant (overall) to pokemon type $i$, which is a good thing. The more positive the better, but any value greater than zero is good.

Hence, we penalize negative $\delta_i$ values more than we reward positive $\delta_i$ values. We define the type-advantage, $TA$, corresponding to the $i$ -th type to be a piecewise function of $\delta_i$. In particular,

$TA_i = -(-\delta_i)^{\frac{3}{2}}$ if $\delta_i < 0$ and $TA_i = \delta_i^{\frac{3}{4}}$ if $\delta_i \geq 0$.

The above $TA$ function penalizes large negative values of $\delta_i$ much more than it penalizes small negative values. On the flip side, it does not reward large positive values of $\delta_i$ much more than it rewards small positive values.

Finally, we define the balance metric of a team as follows: $balance = \sum_iTA_i$. Higher values of balance indicate well-balanced teams, and lower values indicate teams that could use improvement.

The calc_balance function takes the dictionaries returned by calc_weaknesses and calc_resistances as input, and returns the balance resulting from the above calculations.

team_balance = pk.calc_balance(
    resistances=team_resistances, 
    weaknesses=team_weaknesses
)

print(round(team_balance, 3))
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[4], line 1
----> 1 team_balance = pk.calc_balance(
      2     resistances=team_resistances, 
      3     weaknesses=team_weaknesses
      4 )
      6 print(round(team_balance, 3))

File ~/checkouts/readthedocs.org/user_builds/pokehelpyer/envs/stable/lib/python3.9/site-packages/pokehelpyer/pokehelpyer.py:468, in calc_balance(resistances, weaknesses)
    459 assert isinstance(
    460     resistances, dict
    461 ), r"""Input 1 should be a dictionary of resistance values
    462     of the form {pkmn_type: <int or float>}."""
    463 assert isinstance(
    464     weaknesses, dict
    465 ), r"""Input 2 should be a dictionary of weakness values
    466     of the form {pkmn_type: <int or float>}."""
    467 types_set = set(
--> 468     pd.read_csv("data/type_chart.csv")["Attacking"].tolist()
    469 )
    470 assert (
    471     set(resistances.keys()) == types_set
    472 ), """Input 1 should be a dictionary of resistance values obtained
    473     via `calc_resistances`.\n
    474     There should be one key for each of the 18 pokemon types
    475     (including Fairy)."""
    476 assert (
    477     set(weaknesses.keys()) == types_set
    478 ), """Input 2 should be a dictionary of weakness values obtained
    479     via `calc_weaknesses`.\n
    480     There should be one key for each of the 18 pokemon types
    481     (including Fairy)."""

File ~/checkouts/readthedocs.org/user_builds/pokehelpyer/envs/stable/lib/python3.9/site-packages/pandas/util/_decorators.py:211, in deprecate_kwarg.<locals>._deprecate_kwarg.<locals>.wrapper(*args, **kwargs)
    209     else:
    210         kwargs[new_arg_name] = new_arg_value
--> 211 return func(*args, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/pokehelpyer/envs/stable/lib/python3.9/site-packages/pandas/util/_decorators.py:331, in deprecate_nonkeyword_arguments.<locals>.decorate.<locals>.wrapper(*args, **kwargs)
    325 if len(args) > num_allow_args:
    326     warnings.warn(
    327         msg.format(arguments=_format_argument_list(allow_args)),
    328         FutureWarning,
    329         stacklevel=find_stack_level(),
    330     )
--> 331 return func(*args, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/pokehelpyer/envs/stable/lib/python3.9/site-packages/pandas/io/parsers/readers.py:950, in read_csv(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, squeeze, prefix, mangle_dupe_cols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, verbose, skip_blank_lines, parse_dates, infer_datetime_format, keep_date_col, date_parser, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, encoding_errors, dialect, error_bad_lines, warn_bad_lines, on_bad_lines, delim_whitespace, low_memory, memory_map, float_precision, storage_options)
    935 kwds_defaults = _refine_defaults_read(
    936     dialect,
    937     delimiter,
   (...)
    946     defaults={"delimiter": ","},
    947 )
    948 kwds.update(kwds_defaults)
--> 950 return _read(filepath_or_buffer, kwds)

File ~/checkouts/readthedocs.org/user_builds/pokehelpyer/envs/stable/lib/python3.9/site-packages/pandas/io/parsers/readers.py:605, in _read(filepath_or_buffer, kwds)
    602 _validate_names(kwds.get("names", None))
    604 # Create the parser.
--> 605 parser = TextFileReader(filepath_or_buffer, **kwds)
    607 if chunksize or iterator:
    608     return parser

File ~/checkouts/readthedocs.org/user_builds/pokehelpyer/envs/stable/lib/python3.9/site-packages/pandas/io/parsers/readers.py:1442, in TextFileReader.__init__(self, f, engine, **kwds)
   1439     self.options["has_index_names"] = kwds["has_index_names"]
   1441 self.handles: IOHandles | None = None
-> 1442 self._engine = self._make_engine(f, self.engine)

File ~/checkouts/readthedocs.org/user_builds/pokehelpyer/envs/stable/lib/python3.9/site-packages/pandas/io/parsers/readers.py:1735, in TextFileReader._make_engine(self, f, engine)
   1733     if "b" not in mode:
   1734         mode += "b"
-> 1735 self.handles = get_handle(
   1736     f,
   1737     mode,
   1738     encoding=self.options.get("encoding", None),
   1739     compression=self.options.get("compression", None),
   1740     memory_map=self.options.get("memory_map", False),
   1741     is_text=is_text,
   1742     errors=self.options.get("encoding_errors", "strict"),
   1743     storage_options=self.options.get("storage_options", None),
   1744 )
   1745 assert self.handles is not None
   1746 f = self.handles.handle

File ~/checkouts/readthedocs.org/user_builds/pokehelpyer/envs/stable/lib/python3.9/site-packages/pandas/io/common.py:856, in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)
    851 elif isinstance(handle, str):
    852     # Check whether the filename is to be opened in binary mode.
    853     # Binary mode does not support 'encoding' and 'newline'.
    854     if ioargs.encoding and "b" not in ioargs.mode:
    855         # Encoding
--> 856         handle = open(
    857             handle,
    858             ioargs.mode,
    859             encoding=ioargs.encoding,
    860             errors=errors,
    861             newline="",
    862         )
    863     else:
    864         # Binary mode
    865         handle = open(handle, ioargs.mode)

FileNotFoundError: [Errno 2] No such file or directory: 'data/type_chart.csv'

recommend

The goal of the recommend function is the following: "given an input list of up 5 pokemon, determine which pokemon should be added to the team to maximize its 'balance' metric". This is accomplished via a simple brute-force search since the total number of pokemon available is not very large (~700).

Let’s try giving recommend a team of five fire-type pokémon. Since this team is very weak to water-type and rock-type attacks, we should expect recommend to return pokémon that resists water-type and rock-type attacks.

fire_team = ['Charmander', 'Torchic', 'Vulpix', 'Flareon', 'Chimchar']
pk.recommend(fire_team, n_recommendations=2)
Iteration number 1 of 692.
Iteration number 100 of 692.
Iteration number 200 of 692.
Iteration number 300 of 692.
Iteration number 400 of 692.
Iteration number 500 of 692.
Iteration number 600 of 692.
['Skarmory', 'Ferrothorn']

Skarmory is doubly-reistant to rock-type attacks and (normally) resistant to water-type attacks. On the other hand, Ferrothorn is doubly-resistant to water-type attacks and (normally) resistant to rock-type attacks. Adding either of these pokemon to our team of five fire-types would definitely help improve the team’s balance. Looks like the function is working as expected!!