Skip to content

Commit

Permalink
v0.6-beta
Browse files Browse the repository at this point in the history
Separated building and scoring Pokemon into two scripts. package_pokemon.py now fetches data from PokeAPI which takes longer than the previous method but allows for more information to be gathered.
  • Loading branch information
ercdndrs authored Jan 19, 2021
1 parent fbc91ba commit 8689e92
Show file tree
Hide file tree
Showing 3 changed files with 376 additions and 0 deletions.
6 changes: 6 additions & 0 deletions scripts/build_all_data_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import package_pokemon
import score_pokemon

if __name__ == '__main__':
package_pokemon.main()
score_pokemon.main()
293 changes: 293 additions & 0 deletions scripts/package_pokemon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
# Package Pokemon
# Version 2
# Eric Donders
# 2021-01-10
# Read information on rental and boss Pokemon and construct dictionaries which
# are stored as pickle files

import sys
import pickle

from typing import Tuple

import pokebase as pb

# We need to import some class definitions from the parent directory.
from os.path import dirname, abspath, join
base_dir = dirname(dirname(abspath(__file__)))
sys.path.insert(1, base_dir)
from automaxlair import Pokemon, Move


def extract_name_dict(names_resource) -> dict:
"""Takes a names API resource and return a dict with the language as the key
and the name as the value.
"""
name_dict = {}
for entry in names_resource:
name_dict[entry.language.name] = entry.name

return name_dict


def calculate_power(power, name_id: str) -> Tuple[float, float]:
"""Calculate base power and a correction factor that is applied to the base
power of a move. This correction factor accounts for discrepencies between
the base power of a move and its actual damage output in a turn. Examples
where the base power is not reflective include: a) multi-hit moves, b)
multi-turn moves, and c) moves that require certain conditions (e.g.,
Dream Eater).
"""

# The base power is the supplied power except for variable power moves
# where we estimate an average power of 65 and status moves whose power must
# be converted to 0 if it is None.
base_power = power if name_id not in ('electro-ball', 'gyro-ball') else 65
if base_power is None:
base_power = 0

# Calculate correction factor based on matching strings in the effect.
factor = 1
# Account for multi-turn moves.
if (name_id in ('fly', 'dive', 'dig', 'bounce', 'sky-attack', 'solar-beam',
'hyper-beam', 'giga-impact', 'meteor-beam', 'razor-wind')
):
factor = 0.5
elif name_id is 'future-sight':
factor = 0.3
# Account for conditional moves that require certain conditions
# (Steel Roller, Dream Eater, Belch, etc.).
elif name_id in ('steel-roller', 'dream-eater', 'belch'):
factor = 0
elif name_id is 'focus-punch':
factor = 0.75
# Account for multi-strike moves.
elif name_id in ('bonemerang', 'double-hit', 'double-iron-bash',
'double-kick', 'dragon-darts', 'dual-chop', 'dual-wingbeat',
'gear-grind', 'twineedle'
):
factor = 2
elif name_id in ('arm-thrust', 'barrage', 'bone-rush', 'bullet-seed',
'comet-punch', 'double-slap', 'fury-attack', 'fury-swipes',
'icicle-spear', 'pin-missile', 'rock-blast', 'scale-shot',
'spike-cannon', 'tail-slap', 'water-shuriken'
):
factor = 2.2575
elif name_id is 'triple-axel':
factor = 5.23

return base_power, factor


def get_moves(move_resource):
"""Build Move objects (base and Max) from a supplied API resource."""

# Extract important values from the move resource.
id_num = move_resource.id_
name_id = move_resource.name
names = extract_name_dict(move_resource.names)
type_ = move_resource.type.name
category = move_resource.damage_class.name
accuracy_raw = move_resource.accuracy
accuracy = 1 if accuracy_raw is None else accuracy_raw / 100
PP = move_resource.pp
effect = ''
for entry in move_resource.effect_entries:
if entry.language.name == 'en':
effect = entry.short_effect
probability = move_resource.effect_chance
target = move_resource.target.name
is_spread = target in ('all-other-pokemon', 'all-opponents')
base_power, correction_factor = calculate_power(
move_resource.power, name_id
)

# Then create a new Move object (and corresponding Max Move)
# and add it to the list.
move = Move(id_num, name_id, names, type_, category, base_power, accuracy,
PP, effect, probability, is_spread, correction_factor
)
max_move = get_max_move(move)

return move, max_move

def get_max_move(move):
"""Return the Max Move corresponding to the supplied regular move."""

# Change the ID to the ID of the appropriate Max Move.
if move.category == 'status':
name_id = 'max-guard'
type_id = 'normal'
base_power = 0
else:
name_id = {'normal': 'max-strike', 'fighting': 'max-knuckle',
'flying': 'max-airstream', 'poison': 'max-ooze',
'ground': 'max-quake', 'rock': 'max-rockfall',
'bug': 'max-flutterby', 'ghost': 'max-phantasm',
'steel': 'max-steelspike', 'fire': 'max-flare',
'water': 'max-geyser', 'grass': 'max-overgrowth',
'electric': 'max-lightning', 'psychic': 'max-mindstorm',
'ice': 'max-hailstorm', 'dragon': 'max-wyrmwind',
'dark': 'max-darkness', 'fairy': 'max-starfall'
}[move.type_id]
type_id = move.type_id

# Calculate base power based on the original move.
if type_id == 'fighting' or type_id == 'poison':
if move.base_power < 10:
base_power = 0
elif move.base_power <= 40:
base_power = 70
elif move.base_power <= 50:
base_power = 75
elif move.base_power <= 60:
base_power = 80
elif move.base_power <= 70:
base_power = 85
elif move.base_power <= 100:
base_power = 90
elif move.base_power <= 140:
base_power = 95
else:
base_power = 100
else:
if move.base_power < 10:
base_power = 0
elif move.base_power <= 40:
base_power = 90
elif move.base_power <= 50:
base_power = 100
elif move.base_power <= 60:
base_power = 110
elif move.base_power <= 70:
base_power = 120
elif move.base_power <= 100:
base_power = 130
elif move.base_power <= 140:
base_power = 140
else:
base_power = 150


# TODO: update the actual names with translations.

# TODO: look up actual ID of the max move

max_move_resource = pb.APIResource('move', name_id)
id_num = max_move_resource.id_
names = extract_name_dict(max_move_resource.names)

# Instantiate a new move using the parameters calculated above.
max_move = Move(id_num, name_id, names, type_id, move.category, base_power, 1,
move.PP, '', 0
)
return max_move


def pokemon_from_txt(filename: str, level: int) -> dict:
"""Generate a dictionary of Pokemon objects (with their names as the keys)
from a text file.
Each line of the file contains information on the pokemon in the format:
pokemon-name,ability,*move-name where there is a variable number of moves.
"""

pokemon_dict = {}

with open(filename, 'r') as file:
for line in file:
# Unpack each line and then build the corresponding Pokemon using
# PokeAPI (includes stats, names in all languages, et cetera).
name_id, ability_id, *move_ids = line.strip('\n').split(',')


# Information can be supplied as strings (e.g., 'stunfisk-galar')
# or as numeric identifiers. Convert the numbers to ints if
# appropriate.
try:
move_id_list = []
name_id = int(name_id)
ability_id = int(ability_id)
for move_id in move_ids:
move_id_list.append(int(move_id))
move_ids = tuple(move_id_list)
except ValueError:
# IDs were supplied as strings so continue.
pass


# Fetch information on the Pokemon from PokeAPI
variant_resource = pb.APIResource('pokemon', name_id)
variant_name = variant_resource.name
species_name = variant_resource.species.name
species_resource = pb.APIResource('pokemon-species', species_name)
ability_resource = pb.APIResource('ability', ability_id)
id_num = variant_resource.id_
move_resources = []
for move_id in move_ids:
move_resources.append(pb.APIResource('move', move_id))

# Load the Pokemon's types into a list.
type_ids = []
type_names = []
for entry in variant_resource.types:
type_name = entry.type.name
type_ids.append(type_name)
type_resource = pb.APIResource('type', type_name)
type_names.append(extract_name_dict(type_resource.names))

# Load the Pokemon's base stats into a list.
stats = variant_resource.stats
base_stats = [stats[0].base_stat, stats[1].base_stat,
stats[2].base_stat, stats[3].base_stat, stats[4].base_stat,
stats[5].base_stat
]

# Load all of the Pokemon's names into a dict with the language as
# the key.
names = extract_name_dict(species_resource.names)

# Load all of the ability's names into a dict with the language as
# the key.
ability_name_id = ability_resource.name
#ability_num = ability_resource.id_
abilities = extract_name_dict(ability_resource.names)

# Construct a Move object for each move and store it in a list.
moves = []
max_moves = []
for move_resource in move_resources:
move, max_move = get_moves(move_resource)
moves.append(move)
max_moves.append(max_move)

# Finally, create a new Pokemon object from the assembled
# information and add it to the dict.
pokemon = Pokemon(id_num, variant_name, names, ability_name_id,
abilities, type_ids, type_names, base_stats, moves, max_moves,
level=level
)
pokemon_dict[variant_name] = pokemon

#pokemon.print_verbose() # DEBUG
print(f'Finished loading {variant_name}')

return pokemon_dict


def main():
"""Build Pokemon dictionaries from the text files and pickle the results."""

rental_pokemon = pokemon_from_txt(base_dir+'/data/rental_pokemon.txt', 65)
boss_pokemon = pokemon_from_txt(base_dir+'/data/boss_pokemon.txt', 75)

# Pickle the Pokemon dictionaries for later use.
with open(base_dir+'/data/rental_pokemon.pickle', 'wb') as file:
pickle.dump(rental_pokemon, file)
with open(base_dir+'/data/boss_pokemon.pickle', 'wb') as file:
pickle.dump(boss_pokemon, file)
print('Finished packaging Pokemon!')


if __name__ == '__main__':
main()
77 changes: 77 additions & 0 deletions scripts/score_pokemon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Score Pokemon
# Eric Donders
# 2021-01-15

import sys
import pickle

# We need to import some class definitions from the parent directory.
from os.path import dirname, abspath, join
base_dir = dirname(dirname(abspath(__file__)))
sys.path.insert(1, base_dir)
sys.path.insert(1, base_dir+'\\automaxlair')

from automaxlair import Pokemon, Move, matchup_scoring

#import matchup_scoring



def main():
rental_pokemon = pickle.load(open(base_dir+'/data/rental_pokemon.pickle', 'rb'))
boss_pokemon = pickle.load(open(base_dir+'/data/boss_pokemon.pickle', 'rb'))

rental_matchup_LUT = {}
boss_matchup_LUT = {}
rental_pokemon_scores = {}
total_score = 0

# Iterate through all rental Pokemon and calculate scores against all the
# other rental Pokemon and all boss Pokemon. Also calculate an average score
# against other rental Pokemon which is helpful for picking generalists at
# the start of a new run.

for attacker_id in tuple(rental_pokemon):
attacker = rental_pokemon[attacker_id]
# First iterate through all boss Pokemon and score the interactions.
matchups = {}
for defender_id in tuple(boss_pokemon):
defender = boss_pokemon[defender_id]
matchups[defender_id] = matchup_scoring.evaluate_matchup(attacker, defender, rental_pokemon)
boss_matchup_LUT[attacker_id] = matchups

# Then, iterate through all rental Pokemon and score the interactions.
matchups = {}
attacker_score = 0
for defender_id in tuple(rental_pokemon):
defender = rental_pokemon[defender_id]
matchups[defender_id] = matchup_scoring.evaluate_matchup(attacker, defender, rental_pokemon)
# We sum the attacker's score which will later be normalized.
attacker_score += matchups[defender_id]

# Store the scores in a convenient lookup table.
rental_matchup_LUT[attacker_id] = matchups
rental_pokemon_scores[attacker_id] = attacker_score

# Keep a total of all the attackers' scores which is used to normalized
# the individual scores such that the average is 1.
total_score += attacker_score

print(f'Finished computing matchups for {attacker}')

# Normalize the total scores.
for key in rental_pokemon_scores:
rental_pokemon_scores[key] /= (total_score/len(rental_pokemon))


# Pickle the score lookup tables for later use.
with open(base_dir+'/data/boss_matchup_LUT.pickle', 'wb') as file:
pickle.dump(boss_matchup_LUT, file)
with open(base_dir+'/data/rental_matchup_LUT.pickle', 'wb') as file:
pickle.dump(rental_matchup_LUT, file)
with open(base_dir+'/data/rental_pokemon_scores.pickle', 'wb') as file:
pickle.dump(rental_pokemon_scores, file)
print('Finished scoring Pokemon!')

if __name__ == '__main__':
main()

0 comments on commit 8689e92

Please sign in to comment.