diff --git a/.gitignore b/.gitignore index 06973c1249..4721ce0253 100644 --- a/.gitignore +++ b/.gitignore @@ -100,7 +100,6 @@ share/ # PyCharm IDE settings .idea/ -*.iml # Personal load details src/ diff --git a/data/charged_moves.json b/data/charged_moves.json index c3f993191c..3d487b7201 100644 --- a/data/charged_moves.json +++ b/data/charged_moves.json @@ -84,8 +84,9 @@ {"id":101,"name":"Flame Charge","type":"Fire","damage":25,"duration":3100,"energy":20,"dps":8.06}, {"id":34,"name":"Heart Stamp","type":"Psychic","damage":20,"duration":2550,"energy":25,"dps":7.84}, {"id":75,"name":"Parabolic Charge","type":"Electric","damage":15,"duration":2100,"energy":20,"dps":7.14}, +{"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":3700,"energy":20,"dps":6.75}, {"id":111,"name":"Icy Wind","type":"Ice","damage":25,"duration":3800,"energy":20,"dps":6.57}, {"id":84,"name":"Disarming Voice","type":"Fairy","damage":25,"duration":3900,"energy":20,"dps":6.41}, {"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":4000,"energy":20,"dps":6.25}, {"id":66,"name":"Shadow Sneak","type":"Ghost","damage":15,"duration":3100,"energy":20,"dps":4.83}, -{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] +{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] \ No newline at end of file diff --git a/data/level_to_cpm.json b/data/level_to_cpm.json deleted file mode 100644 index d2483d9a41..0000000000 --- a/data/level_to_cpm.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "1": 0.094, - "1.5": 0.135137432, - "2": 0.16639787, - "2.5": 0.192650919, - "3": 0.21573247, - "3.5": 0.236572661, - "4": 0.25572005, - "4.5": 0.273530381, - "5": 0.29024988, - "5.5": 0.306057377, - "6": 0.3210876, - "6.5": 0.335445036, - "7": 0.34921268, - "7.5": 0.362457751, - "8": 0.37523559, - "8.5": 0.387592406, - "9": 0.39956728, - "9.5": 0.411193551, - "10": 0.42250001, - "10.5": 0.432926419, - "11": 0.44310755, - "11.5": 0.4530599578, - "12": 0.46279839, - "12.5": 0.472336083, - "13": 0.48168495, - "13.5": 0.4908558, - "14": 0.49985844, - "14.5": 0.508701765, - "15": 0.51739395, - "15.5": 0.525942511, - "16": 0.53435433, - "16.5": 0.542635767, - "17": 0.55079269, - "17.5": 0.558830576, - "18": 0.56675452, - "18.5": 0.574569153, - "19": 0.58227891, - "19.5": 0.589887917, - "20": 0.59740001, - "20.5": 0.604818814, - "21": 0.61215729, - "21.5": 0.619399365, - "22": 0.62656713, - "22.5": 0.633644533, - "23": 0.64065295, - "23.5": 0.647576426, - "24": 0.65443563, - "24.5": 0.661214806, - "25": 0.667934, - "25.5": 0.674577537, - "26": 0.68116492, - "26.5": 0.687680648, - "27": 0.69414365, - "27.5": 0.700538673, - "28": 0.70688421, - "28.5": 0.713164996, - "29": 0.71939909, - "29.5": 0.725571552, - "30": 0.7317, - "30.5": 0.734741009, - "31": 0.73776948, - "31.5": 0.740785574, - "32": 0.74378943, - "32.5": 0.746781211, - "33": 0.74976104, - "33.5": 0.752729087, - "34": 0.75568551, - "34.5": 0.758630378, - "35": 0.76156384, - "35.5": 0.764486065, - "36": 0.76739717, - "36.5": 0.770297266, - "37": 0.7731865, - "37.5": 0.776064962, - "38": 0.77893275, - "38.5": 0.781790055, - "39": 0.78463697, - "39.5": 0.787473578, - "40": 0.79030001 -} \ No newline at end of file diff --git a/data/pokemon.json b/data/pokemon.json index 8ccc906d02..e64b7c31fb 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -1232,7 +1232,7 @@ "Previous evolution(s)": [ { "Number": "029", - "Name": "Nidoran F" + "Name": "Nidoran F" } ], "Next Evolution Requirements": { @@ -4326,14 +4326,20 @@ ], "Weight": "49.8 kg", "Height": "1.5 m", + "Next evolution(s)": [ + { + "Number": "107", + "Name": "Hitmonchan" + } + ], "Special Attack(s)": [ "Low Sweep", "Stomp", "Stone Edge" ], "BaseAttack": 148, - "BaseDefense": 172, - "BaseStamina": 100, + "BaseDefense": 100, + "BaseStamina": 172, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -4355,6 +4361,12 @@ ], "Weight": "50.2 kg", "Height": "1.4 m", + "Previous evolution(s)": [ + { + "Number": "106", + "Name": "Hitmonlee" + } + ], "Special Attack(s)": [ "Brick Break", "Fire Punch", @@ -4362,8 +4374,8 @@ "Thunder Punch" ], "BaseAttack": 138, - "BaseDefense": 204, - "BaseStamina": 100, + "BaseDefense": 100, + "BaseStamina": 204, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -5717,16 +5729,6 @@ "Family": 147, "Name": "Dratini candies" }, - "Next evolution(s)": [ - { - "Number": "148", - "Name": "Dragonair" - }, - { - "Number": "149", - "Name": "Dragonite" - } - ], "Special Attack(s)": [ "Aqua Tail", "Twister", diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index b27eaa388a..ea81b7c093 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1,45 +1,26 @@ import json import os - from pokemongo_bot.base_dir import _base_dir ''' Helper class for updating/retrieving Inventory data ''' - -# -# Abstraction - -class _StaticInventoryComponent(object): +class _BaseInventoryComponent(object): + TYPE = None # base key name for items of this type + ID_FIELD = None # identifier field for items of this type STATIC_DATA_FILE = None # optionally load static data from file, # dropping the data in a static variable named STATIC_DATA - STATIC_DATA = None def __init__(self): + self._data = {} if self.STATIC_DATA_FILE is not None: self.init_static_data() @classmethod def init_static_data(cls): if not hasattr(cls, 'STATIC_DATA') or cls.STATIC_DATA is None: - cls.STATIC_DATA = cls.process_static_data( - json.load(open(cls.STATIC_DATA_FILE))) - - @classmethod - def process_static_data(cls, data): - # optional hook for processing the static data - # default is to use the data directly - return data - - -class _BaseInventoryComponent(_StaticInventoryComponent): - TYPE = None # base key name for items of this type - ID_FIELD = None # identifier field for items of this type - - def __init__(self): - self._data = {} - super(_BaseInventoryComponent, self).__init__() + cls.STATIC_DATA = json.load(open(cls.STATIC_DATA_FILE)) def parse(self, item): # optional hook for parsing the dict for this item @@ -61,22 +42,34 @@ def retrieve_data(self, inventory): def refresh(self, inventory): self._data = self.retrieve_data(inventory) - def get(self, object_id): - return self._data.get(object_id) + def get(self, id): + return self._data.get(id) def all(self): return list(self._data.values()) -# -# Inventory Components +class Candy(object): + def __init__(self, family_id, quantity): + self.type = Pokemons.name_for(family_id) + self.quantity = quantity + + def consume(self, amount): + if self.quantity < amount: + raise Exception('Tried to consume more {} candy than you have'.format(self.type)) + self.quantity -= amount + + def add(self, amount): + if amount < 0: + raise Exception('Must add positive amount of candy') + self.quantity += amount class Candies(_BaseInventoryComponent): TYPE = 'candy' ID_FIELD = 'family_id' @classmethod - def family_id_for(cls, pokemon_id): + def family_id_for(self, pokemon_id): return Pokemons.first_evolution_id_for(pokemon_id) def get(self, pokemon_id): @@ -115,120 +108,17 @@ class Pokemons(_BaseInventoryComponent): ID_FIELD = 'id' STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'pokemon.json') - @classmethod - def process_static_data(cls, data): - pokemon_id = 1 - for poke_info in data: - # prepare types - types = [poke_info['Type I'][0]] # required - for t in poke_info.get('Type II', []): - types.append(t) - poke_info['types'] = types - - # prepare attacks (moves) - cls._process_attacks(poke_info) - cls._process_attacks(poke_info, charged=True) - - # prepare movesets - poke_info['movesets'] = cls._process_movesets(poke_info, pokemon_id) - - # calculate maximum CP for the pokemon (best IVs, lvl 40) - base_attack = poke_info['BaseAttack'] - base_defense = poke_info['BaseDefense'] - base_stamina = poke_info['BaseStamina'] - max_cp = _calc_cp(base_attack, base_defense, base_stamina) - poke_info['max_cp'] = max_cp - - pokemon_id += 1 - return data - - @classmethod - def _process_movesets(cls, poke_info, pokemon_id): - # type: (dict, int) -> List[Moveset] - """ - The optimal moveset is the combination of two moves, one quick move - and one charge move, that deals the most damage over time. - - Because each quick move gains a certain amount of energy (different - for different moves) and each charge move requires a different amount - of energy to use, sometimes, a quick move with lower DPS will be - better since it charges the charge move faster. On the same note, - sometimes a charge move that has lower DPS will be more optimal since - it may require less energy or it may last for a longer period of time. - - Attacker have STAB (Same-type attack bonus - x1.25) pokemon have the - same type as attack. So we add it to the "Combo DPS" of the moveset. - - The defender attacks in intervals of 1 second for the first 2 attacks, - and then in intervals of 2 seconds for the remainder of the attacks. - This explains why we see two consecutive quick attacks at the beginning - of the match. As a result, we add +2 seconds to the DPS calculation - for defender DPS output. - - So to determine an optimal defensive moveset, we follow the same method - as we did for optimal offensive movesets, but instead calculate the - highest "Combo DPS" with an added 2 seconds to the quick move cool down. - - Note: critical hits have not yet been implemented in the game - - See http://pokemongo.gamepress.gg/optimal-moveset-explanation - See http://pokemongo.gamepress.gg/defensive-tactics - """ - - # Prepare movesets - movesets = [] - types = poke_info['types'] - for fm in poke_info['Fast Attack(s)']: - for chm in poke_info['Special Attack(s)']: - movesets.append(Moveset(fm, chm, types, pokemon_id)) - assert len(movesets) > 0 - - # Calculate attack perfection for each moveset - movesets = sorted(movesets, key=lambda m: m.dps_attack) - worst_dps = movesets[0].dps_attack - best_dps = movesets[-1].dps_attack - if best_dps > worst_dps: - for moveset in movesets: - current_dps = moveset.dps_attack - moveset.attack_perfection = \ - (current_dps - worst_dps) / (best_dps - worst_dps) - - # Calculate defense perfection for each moveset - movesets = sorted(movesets, key=lambda m: m.dps_defense) - worst_dps = movesets[0].dps_defense - best_dps = movesets[-1].dps_defense - if best_dps > worst_dps: - for moveset in movesets: - current_dps = moveset.dps_defense - moveset.defense_perfection = \ - (current_dps - worst_dps) / (best_dps - worst_dps) - - return sorted(movesets, key=lambda m: m.dps, reverse=True) - - @classmethod - def _process_attacks(cls, poke_info, charged=False): - # type: (dict, bool) -> List[Attack] - key = 'Fast Attack(s)' if not charged else 'Special Attack(s)' - moves_dict = (ChargedAttacks if charged else FastAttacks).BY_NAME - moves = [] - for name in poke_info[key]: - if name not in moves_dict: - raise KeyError('Unknown {} attack: "{}"'.format( - 'charged' if charged else 'fast', name)) - moves.append(moves_dict[name]) - moves = sorted(moves, key=lambda m: m.dps, reverse=True) - poke_info[key] = moves - assert len(moves) > 0 - return moves + def parse(self, item): + if 'is_egg' in item: + return Egg(item) + return Pokemon(item) @classmethod def data_for(cls, pokemon_id): - # type: (int) -> dict return cls.STATIC_DATA[pokemon_id - 1] @classmethod def name_for(cls, pokemon_id): - # type: (int) -> string return cls.data_for(pokemon_id)['Name'] @classmethod @@ -239,194 +129,24 @@ def first_evolution_id_for(cls, pokemon_id): return pokemon_id @classmethod - def prev_evolution_id_for(cls, pokemon_id): - data = cls.data_for(pokemon_id) - if 'Previous evolution(s)' in data: - return int(data['Previous evolution(s)'][-1]['Number']) - return None - - @classmethod - def next_evolution_ids_for(cls, pokemon_id): + def next_evolution_id_for(cls, pokemon_id): try: - next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] + return int(cls.data_for(pokemon_id)['Next evolution(s)'][0]['Number']) except KeyError: - return [] - # get only next level evolutions, not all possible - ids = [] - for p in next_evolutions: - p_id = int(p['Number']) - if cls.prev_evolution_id_for(p_id) == pokemon_id: - ids.append(p_id) - return ids + return None @classmethod - def last_evolution_ids_for(cls, pokemon_id): + def evolution_cost_for(cls, pokemon_id): try: - next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] + return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) except KeyError: - return [pokemon_id] - # get only final evolutions, not all possible - ids = [] - for p in next_evolutions: - p_id = int(p['Number']) - if len(cls.data_for(p_id).get('Next evolution(s)', [])) == 0: - ids.append(p_id) - assert len(ids) > 0 - return ids - - @classmethod - def has_next_evolution(cls, pokemon_id): - poke_info = cls.data_for(pokemon_id) - return 'Next Evolution Requirements' in poke_info \ - or 'Next evolution(s)' in poke_info - - @classmethod - def evolution_cost_for(cls, pokemon_id): - if not cls.has_next_evolution(pokemon_id): - return None - return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) - - def parse(self, item): - if 'is_egg' in item: - return Egg(item) - return Pokemon(item) + return def all(self): # by default don't include eggs in all pokemon (usually just # makes caller's lives more difficult) return [p for p in super(Pokemons, self).all() if not isinstance(p, Egg)] - -# -# Static Components - -class LevelToCPm(_StaticInventoryComponent): - """ - Data for the CP multipliers at different levels - See http://pokemongo.gamepress.gg/cp-multiplier - See https://github.com/justinleewells/pogo-optimizer/blob/edd692d/data/game/level-to-cpm.json - """ - - STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'level_to_cpm.json') - MAX_LEVEL = 40 - MAX_CPM = .0 - # half of the lowest difference between CPMs - HALF_DIFF_BETWEEN_HALF_LVL = 14e-3 - - @classmethod - def init_static_data(cls): - super(LevelToCPm, cls).init_static_data() - cls.MAX_CPM = cls.cp_multiplier_for(cls.MAX_LEVEL) - - @classmethod - def cp_multiplier_for(cls, level): - # type: (Union[float, int, string]) -> float - level = float(level) - level = str(int(level) if level.is_integer() else level) - return cls.STATIC_DATA[level] - - @classmethod - def level_from_cpm(cls, cp_multiplier): - # type: (float) -> float - for lvl, cpm in cls.STATIC_DATA.iteritems(): - diff = abs(cpm - cp_multiplier) - if diff <= cls.HALF_DIFF_BETWEEN_HALF_LVL: - return float(lvl) - raise ValueError("Unknown cp_multiplier: {}".format(cp_multiplier)) - - -class _Attacks(_StaticInventoryComponent): - BY_NAME = {} # type: Dict[string, Attack] - BY_TYPE = {} # type: Dict[List[Attack]] - BY_DPS = [] # type: List[Attack] - - @classmethod - def process_static_data(cls, moves): - ret = {} - by_type = {} - by_name = {} - fast = cls is FastAttacks - for attack in moves: - attack = Attack(attack) if fast else ChargedAttack(attack) - ret[attack.id] = attack - by_name[attack.name] = attack - - if attack.type not in by_type: - by_type[attack.type] = [] - by_type[attack.type].append(attack) - - for t in by_type.iterkeys(): - attacks = sorted(by_type[t], key=lambda m: m.dps, reverse=True) - min_dps = attacks[-1].dps - max_dps = attacks[0].dps - min_dps - if max_dps > .0: - for attack in attacks: # type: Attack - attack.rate_in_type = (attack.dps - min_dps) / max_dps - by_type[t] = attacks - - cls.BY_NAME = by_name - cls.BY_TYPE = by_type - cls.BY_DPS = sorted(ret.values(), key=lambda m: m.dps, reverse=True) - - return ret - - @classmethod - def data_for(cls, attack_id): - # type: (int) -> Attack - if attack_id not in cls.STATIC_DATA: - raise ValueError("Attack {} not found in {}".format( - attack_id, cls.__name__)) - return cls.STATIC_DATA[attack_id] - - @classmethod - def by_name(cls, name): - # type: (string) -> Attack - return cls.BY_NAME[name] - - @classmethod - def list_for_type(cls, type_name): - # type: (string) -> List[Attack] - """ - :return: Attacks sorted by DPS in descending order - """ - return cls.BY_TYPE[type_name] - - @classmethod - def all(cls): - return cls.STATIC_DATA.values() - - @classmethod - def all_by_dps(cls): - return cls.BY_DPS - - -class FastAttacks(_Attacks): - STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'fast_moves.json') - - -class ChargedAttacks(_Attacks): - STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'charged_moves.json') - - -# -# Instances - -class Candy(object): - def __init__(self, family_id, quantity): - self.type = Pokemons.name_for(family_id) - self.quantity = quantity - - def consume(self, amount): - if self.quantity < amount: - raise Exception('Tried to consume more {} candy than you have'.format(self.type)) - self.quantity -= amount - - def add(self, amount): - if amount < 0: - raise Exception('Must add positive amount of candy') - self.quantity += amount - - class Egg(object): def __init__(self, data): self._data = data @@ -438,297 +158,52 @@ def has_next_evolution(self): class Pokemon(object): def __init__(self, data): self._data = data - # Unique ID for this particular Pokemon self.id = data['id'] - # Id of the such pokemons in pokedex self.pokemon_id = data['pokemon_id'] - - # Combat points value self.cp = data['cp'] - # Base CP multiplier, fixed at the catch time - self.cp_bm = data['cp_multiplier'] - # Changeable part of the CP multiplier, increasing at power up - self.cp_am = data.get('additional_cp_multiplier', .0) - # Resulting CP multiplier - self.cp_m = self.cp_bm + self.cp_am - - # Current pokemon level (half of level is a normal value) - self.level = LevelToCPm.level_from_cpm(self.cp_m) - - # Maximum health points - self.hp_max = data['stamina_max'] - # Current health points - self.hp = data.get('stamina', self.hp_max) - assert 0 <= self.hp <= self.hp_max - - # Individial Values of the current pokemon (different for each pokemon) - self.iv_attack = data.get('individual_attack', 0) - self.iv_defense = data.get('individual_defense', 0) - self.iv_stamina = data.get('individual_stamina', 0) - self._static_data = Pokemons.data_for(self.pokemon_id) self.name = Pokemons.name_for(self.pokemon_id) - self.nickname = data.get('nickname', self.name) - + self.iv = self._compute_iv() self.in_fort = 'deployed_fort_id' in data self.is_favorite = data.get('favorite', 0) is 1 - # Basic Values of the current pokemon (identical for all such pokemons) - self.base_attack = self._static_data['BaseAttack'] - self.base_defense = self._static_data['BaseDefense'] - self.base_stamina = self._static_data['BaseStamina'] - - # Maximum possible CP for the current pokemon - self.max_cp = self._static_data['max_cp'] - - self.fast_attack = FastAttacks.data_for(data['move_1']) - self.charged_attack = ChargedAttacks.data_for(data['move_2']) - - # Internal values (IV) perfection percent - self.iv = self._compute_iv_perfection() - - # IV CP perfection - kind of IV perfection percent but calculated - # using weight of each IV in its contribution to CP of the best - # evolution of current pokemon - # So it tends to be more accurate than simple IV perfection - self.ivcp = self._compute_cp_perfection() - - # Exact value of current CP (not rounded) - self.cp_exact = _calc_cp( - self.base_attack, self.base_defense, self.base_stamina, - self.iv_attack, self.iv_defense, self.iv_stamina, self.cp_m) - - # Percent of maximum possible CP - self.cp_percent = self.cp_exact / self.max_cp - - # Get moveset instance with calculated DPS and perfection percents - self.moveset = self._get_moveset() - - def __str__(self): - return self.name - - def __repr__(self): - return self.name - def can_evolve_now(self): - return self.has_next_evolution() and \ - self.candy_quantity >= self.evolution_cost + return self.has_next_evolution() and self.candy_quantity >= self.evolution_cost def has_next_evolution(self): - return Pokemons.has_next_evolution(self.pokemon_id) + return 'Next Evolution Requirements' in self._static_data def has_seen_next_evolution(self): - for pokemon_id in self.next_evolution_ids: - if pokedex().captured(pokemon_id): - return True - return False + return pokedex().captured(self.next_evolution_id) @property - def family_id(self): - return self.first_evolution_id + def next_evolution_id(self): + return Pokemons.next_evolution_id_for(self.pokemon_id) @property def first_evolution_id(self): return Pokemons.first_evolution_id_for(self.pokemon_id) - @property - def prev_evolution_id(self): - return Pokemons.prev_evolution_id_for(self.pokemon_id) - - @property - def next_evolution_ids(self): - return Pokemons.next_evolution_ids_for(self.pokemon_id) - - @property - def last_evolution_ids(self): - return Pokemons.last_evolution_ids_for(self.pokemon_id) - @property def candy_quantity(self): return candies().get(self.pokemon_id).quantity @property def evolution_cost(self): - return Pokemons.evolution_cost_for(self.pokemon_id) - - def _compute_iv_perfection(self): - total_iv = self.iv_attack + self.iv_defense + self.iv_stamina - iv_perfection = round((total_iv / 45.0), 2) - return iv_perfection - - def _compute_cp_perfection(self): - """ - CP perfect percent is more accurate than IV perfect - - We know attack plays an important role in CP, and different - pokemons have different base value, that's means 15/14/15 is - better than 14/15/15 for lot of pokemons, and if one pokemon's - base def is more than base sta, 15/15/14 is better than 15/14/15. - - See https://github.com/jabbink/PokemonGoBot/issues/469 - - So calculate CP perfection at final level for the best of the final - evolutions of the pokemon. - """ - variants = [] - iv_attack = self.iv_attack - iv_defense = self.iv_defense - iv_stamina = self.iv_stamina - cp_m = LevelToCPm.MAX_CPM - last_evolution_ids = self.last_evolution_ids - for pokemon_id in last_evolution_ids: - poke_info = Pokemons.data_for(pokemon_id) - base_attack = poke_info['BaseAttack'] - base_defense = poke_info['BaseDefense'] - base_stamina = poke_info['BaseStamina'] - - # calculate CP variants at maximum level - worst_cp = _calc_cp(base_attack, base_defense, base_stamina, - 0, 0, 0, cp_m) - perfect_cp = _calc_cp(base_attack, base_defense, base_stamina, - cp_multiplier=cp_m) - current_cp = _calc_cp(base_attack, base_defense, base_stamina, - iv_attack, iv_defense, iv_stamina, cp_m) - cp_perfection = (current_cp - worst_cp) / (perfect_cp - worst_cp) - variants.append(cp_perfection) - - # get best value (probably for the best evolution) - cp_perfection = max(variants) - return cp_perfection - - def _get_moveset(self): - move1 = self.fast_attack - move2 = self.charged_attack - movesets = self._static_data['movesets'] - current_moveset = None - for moveset in movesets: # type: Moveset - if moveset.fast_attack == move1 and moveset.charged_attack == move2: - current_moveset = moveset - break - - if current_moveset is None: - raise Exception("Unexpected moveset [{}, {}] for #{} {}".format( - move1, move2, self.pokemon_id, self.name)) - - return current_moveset - - -class Attack(object): - def __init__(self, data): - # self._data = data # Not needed - all saved in fields - self.id = data['id'] - self.name = data['name'] - self.type = data['type'] - self.damage = data['damage'] - self.duration = data['duration'] / 1000.0 # duration in seconds - - # Energy addition for fast attack - # Energy cost for charged attack - self.energy = data['energy'] - - # Damage Per Second - # recalc for better precision - self.dps = self.damage / self.duration - - # Perfection of the attack in it's type (from 0 to 1) - self.rate_in_type = .0 - - @property - def damage_with_stab(self): - # damage with STAB (Same-type attack bonus) - return self.damage * STAB_FACTOR - - @property - def dps_with_stab(self): - # DPS with STAB (Same-type attack bonus) - return self.dps * STAB_FACTOR - - @property - def energy_per_second(self): - return self.energy / self.duration - - @property - def dodge_window(self): - # TODO: Attack Dodge Window - return NotImplemented - - @property - def is_charged(self): - return False - - def __str__(self): - return self.name - - def __repr__(self): - return self.name - - -class ChargedAttack(Attack): - def __init__(self, data): - super(ChargedAttack, self).__init__(data) - - @property - def is_charged(self): - return True - - -class Moveset(object): - def __init__(self, fm, chm, pokemon_types=(), pokemon_id=-1): - # type: (Attack, ChargedAttack, List[string], int) -> None - self.pokemon_id = pokemon_id - self.fast_attack = fm - self.charged_attack = chm - - # See Pokemons._process_movesets() - # See http://pokemongo.gamepress.gg/optimal-moveset-explanation - # See http://pokemongo.gamepress.gg/defensive-tactics - - fm_number = 100 # for simplicity we use 100 - - fm_energy = fm.energy * fm_number - fm_damage = fm.damage * fm_number - fm_secs = fm.duration * fm_number - - # Defender attacks in intervals of 1 second for the - # first 2 attacks, and then in intervals of 2 seconds - # So add 1.95 seconds to the quick move cool down for defense - # 1.95 is something like an average here - # TODO: Do something better? - fm_defense_secs = (fm.duration + 1.95) * fm_number - - chm_number = fm_energy / chm.energy - chm_damage = chm.damage * chm_number - chm_secs = chm.duration * chm_number - - damage_sum = fm_damage + chm_damage - # raw Damage-Per-Second for the moveset - self.dps = damage_sum / (fm_secs + chm_secs) - # average DPS for defense - self.dps_defense = damage_sum / (fm_defense_secs + chm_secs) - - # apply STAB (Same-type attack bonus) - if fm.type in pokemon_types: - fm_damage *= STAB_FACTOR - if chm.type in pokemon_types: - chm_damage *= STAB_FACTOR - - # DPS for attack (counting STAB) - self.dps_attack = (fm_damage + chm_damage) / (fm_secs + chm_secs) + return self._static_data['Next Evolution Requirements']['Amount'] - # Moveset perfection percent attack and for defense - # Calculated for current pokemon, not between all pokemons - # So 100% perfect moveset can be weak if pokemon is weak (e.g. Caterpie) - self.attack_perfection = .0 - self.defense_perfection = .0 + def _compute_iv(self): + total_IV = 0.0 + iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - # TODO: True DPS for real combat (floor(Attack/200 * MovePower * STAB) + 1) - # See http://pokemongo.gamepress.gg/pokemon-attack-explanation - - def __str__(self): - return '[{}, {}]'.format(self.fast_attack, self.charged_attack) - - def __repr__(self): - return '[{}, {}]'.format(self.fast_attack, self.charged_attack) + for individual_stat in iv_stats: + try: + total_IV += self._data[individual_stat] + except Exception: + self._data[individual_stat] = 0 + continue + pokemon_potential = round((total_IV / 45.0), 2) + return pokemon_potential class Inventory(object): @@ -752,47 +227,8 @@ def refresh(self): with open(user_web_inventory, 'w') as outfile: json.dump(inventory, outfile) -# -# Usage helpers - -# STAB (Same-type attack bonus) -STAB_FACTOR = 1.25 _inventory = None -LevelToCPm() # init LevelToCPm -FastAttacks() # init FastAttacks -ChargedAttacks() # init ChargedAttacks - - -def _calc_cp(base_attack, base_defense, base_stamina, - iv_attack=15, iv_defense=15, iv_stamina=15, - cp_multiplier=LevelToCPm.MAX_CPM): - """ - CP calculation - - CP = (Attack * Defense^0.5 * Stamina^0.5 * CP_Multiplier^2) / 10 - CP = (BaseAtk+AtkIV) * (BaseDef+DefIV)^0.5 * (BaseStam+StamIV)^0.5 * Lvl(CPScalar)^2 / 10 - - See https://www.reddit.com/r/TheSilphRoad/comments/4t7r4d/exact_pokemon_cp_formula/ - See https://www.reddit.com/r/pokemongodev/comments/4t7xb4/exact_cp_formula_from_stats_and_cpm_and_an_update/ - See http://pokemongo.gamepress.gg/pokemon-stats-advanced - See http://pokemongo.gamepress.gg/cp-multiplier - See http://gaming.stackexchange.com/questions/280491/formula-to-calculate-pokemon-go-cp-and-hp - - :param base_attack: Pokemon BaseAttack - :param base_defense: Pokemon BaseDefense - :param base_stamina: Pokemon BaseStamina - :param iv_attack: Pokemon IndividualAttack (0..15) - :param iv_defense: Pokemon IndividualDefense (0..15) - :param iv_stamina: Pokemon IndividualStamina (0..15) - :param cp_multiplier: CP Multiplier (0.79030001 is max - value for level 40) - :return: CP as float - """ - return (base_attack + iv_attack) \ - * ((base_defense + iv_defense)**0.5) \ - * ((base_stamina + iv_stamina)**0.5) \ - * (cp_multiplier ** 2) / 10 - def init_inventory(bot): global _inventory @@ -821,15 +257,3 @@ def pokemons(refresh=False): def items(): return _inventory.items - - -def levels_to_cpm(): - return LevelToCPm - - -def fast_attacks(): - return FastAttacks - - -def charged_attacks(): - return ChargedAttacks diff --git a/tests/inventory_test.py b/tests/inventory_test.py deleted file mode 100644 index 8362ce2b91..0000000000 --- a/tests/inventory_test.py +++ /dev/null @@ -1,183 +0,0 @@ -import unittest - -from pokemongo_bot.inventory import * - - -class InventoryTest(unittest.TestCase): - def test_pokemons(self): - # Init data - self.assertEqual(len(Pokemons().all()), 0) # No inventory loaded here - - obj = Pokemons - self.assertEqual(len(obj.STATIC_DATA), 151) - - for poke_info in obj.STATIC_DATA: - name = poke_info['Name'] - pokemon_id = int(poke_info['Number']) - self.assertTrue(1 <= pokemon_id <= 151) - - self.assertGreaterEqual(len(poke_info['movesets']), 1) - self.assertTrue(262 <= poke_info['max_cp'] <= 4145) - self.assertTrue(1 <= len(poke_info['types']) <= 2) - self.assertTrue(40 <= poke_info['BaseAttack'] <= 284) - self.assertTrue(20 <= poke_info['BaseDefense'] <= 500) - self.assertTrue(54 <= poke_info['BaseStamina'] <= 242) - self.assertTrue(.0 <= poke_info['CaptureRate'] <= .56) - self.assertTrue(.0 <= poke_info['FleeRate'] <= .99) - self.assertTrue(1 <= len(poke_info['Weaknesses']) <= 7) - self.assertTrue(3 <= len(name) <= 10) - - self.assertGreaterEqual(len(poke_info['Classification']), 11) - self.assertGreaterEqual(len(poke_info['Fast Attack(s)']), 1) - self.assertGreaterEqual(len(poke_info['Special Attack(s)']), 1) - - self.assertIs(obj.data_for(pokemon_id), poke_info) - self.assertIs(obj.name_for(pokemon_id), name) - - first_evolution_id = obj.first_evolution_id_for(pokemon_id) - self.assertGreaterEqual(first_evolution_id, 1) - next_evolution_ids = obj.next_evolution_ids_for(pokemon_id) - last_evolution_ids = obj.last_evolution_ids_for(pokemon_id) - candies_cost = obj.evolution_cost_for(pokemon_id) - obj.prev_evolution_id_for(pokemon_id) # just call test - self.assertGreaterEqual(len(last_evolution_ids), 1) - - if not obj.has_next_evolution(pokemon_id): - assert 'Next evolution(s)' not in poke_info - assert 'Next Evolution Requirements' not in poke_info - else: - self.assertGreaterEqual(len(next_evolution_ids), 1) - self.assertLessEqual(len(next_evolution_ids), len(last_evolution_ids)) - - reqs = poke_info['Next Evolution Requirements'] - self.assertEqual(reqs["Family"], first_evolution_id) - candies_name = obj.name_for(first_evolution_id) + ' candies' - self.assertEqual(reqs["Name"], candies_name) - self.assertIsNotNone(candies_cost) - self.assertTrue(12 <= candies_cost <= 400) - self.assertEqual(reqs["Amount"], candies_cost) - - evolutions = poke_info["Next evolution(s)"] - self.assertGreaterEqual(len(evolutions), len(next_evolution_ids)) - - for p in evolutions: - p_id = int(p["Number"]) - self.assertNotEqual(p_id, pokemon_id) - self.assertEqual(p["Name"], obj.name_for(p_id)) - - for p_id in next_evolution_ids: - self.assertEqual(obj.prev_evolution_id_for(p_id), pokemon_id) - prev_evs = obj.data_for(p_id)["Previous evolution(s)"] - self.assertGreaterEqual(len(prev_evs), 1) - self.assertEqual(int(prev_evs[-1]["Number"]), pokemon_id) - self.assertEqual(prev_evs[-1]["Name"], name) - - # Only Eevee has 3 next evolutions - self.assertEqual(len(next_evolution_ids), - 1 if pokemon_id != 133 else 3) - - if "Previous evolution(s)" in poke_info: - for p in poke_info["Previous evolution(s)"]: - p_id = int(p["Number"]) - self.assertNotEqual(p_id, pokemon_id) - self.assertEqual(p["Name"], obj.name_for(p_id)) - - # - # Specific pokemons testing - - poke = Pokemon({ - "num_upgrades": 2, "move_1": 210, "move_2": 69, "pokeball": 2, - "favorite": 1, "pokemon_id": 42, "battles_attacked": 4, - "stamina": 76, "stamina_max": 76, "individual_attack": 9, - "individual_defense": 4, "individual_stamina": 8, - "cp_multiplier": 0.4627983868122101, - "additional_cp_multiplier": 0.018886566162109375, - "cp": 653, "nickname": "Golb", "id": 13632861873471324}) - self.assertEqual(poke.level, 12.5) - self.assertEqual(poke.iv, 0.47) - self.assertAlmostEqual(poke.ivcp, 0.482845351) - self.assertAlmostEqual(poke.max_cp, 1921.34561459) - self.assertAlmostEqual(poke.cp_percent, 0.34000973) - self.assertTrue(poke.is_favorite) - self.assertEqual(poke.name, 'Golbat') - self.assertEqual(poke.nickname, "Golb") - self.assertAlmostEqual(poke.moveset.dps, 10.7540173053) - self.assertAlmostEqual(poke.moveset.dps_attack, 12.14462299) - self.assertAlmostEqual(poke.moveset.dps_defense, 4.876681614) - self.assertAlmostEqual(poke.moveset.attack_perfection, 0.4720730048) - self.assertAlmostEqual(poke.moveset.defense_perfection, 0.8158081497) - - poke = Pokemon({ - "move_1": 221, "move_2": 129, "pokemon_id": 19, "cp": 110, - "individual_attack": 6, "stamina_max": 22, "individual_defense": 14, - "cp_multiplier": 0.37523558735847473, "id": 7841053399}) - self.assertEqual(poke.level, 7.5) - self.assertEqual(poke.iv, 0.44) - self.assertAlmostEqual(poke.ivcp, 0.452398293) - self.assertAlmostEqual(poke.max_cp, 581.64643575) - self.assertAlmostEqual(poke.cp_percent, 0.189251848608) - self.assertFalse(poke.is_favorite) - self.assertEqual(poke.name, 'Rattata') - self.assertEqual(poke.nickname, 'Rattata') - self.assertAlmostEqual(poke.moveset.dps, 12.5567813108) - self.assertAlmostEqual(poke.moveset.dps_attack, 15.6959766385) - self.assertAlmostEqual(poke.moveset.dps_defense, 5.54282440561) - self.assertAlmostEqual(poke.moveset.attack_perfection, 0.835172881385) - self.assertAlmostEqual(poke.moveset.defense_perfection, 0.603137650999) - - def test_levels_to_cpm(self): - l2c = LevelToCPm - self.assertIs(levels_to_cpm(), l2c) - max_cpm = l2c.cp_multiplier_for(l2c.MAX_LEVEL) - self.assertEqual(l2c.MAX_LEVEL, 40) - self.assertEqual(l2c.MAX_CPM, max_cpm) - self.assertEqual(len(l2c.STATIC_DATA), 79) - - self.assertEqual(l2c.cp_multiplier_for("1"), 0.094) - self.assertEqual(l2c.cp_multiplier_for(1), 0.094) - self.assertEqual(l2c.cp_multiplier_for(1.0), 0.094) - self.assertEqual(l2c.cp_multiplier_for("17.5"), 0.558830576) - self.assertEqual(l2c.cp_multiplier_for(17.5), 0.558830576) - self.assertEqual(l2c.cp_multiplier_for('40.0'), 0.79030001) - self.assertEqual(l2c.cp_multiplier_for(40.0), 0.79030001) - self.assertEqual(l2c.cp_multiplier_for(40), 0.79030001) - - self.assertEqual(l2c.level_from_cpm(0.79030001), 40.0) - self.assertEqual(l2c.level_from_cpm(0.7903), 40.0) - - def test_attacks(self): - self._test_attacks(fast_attacks, FastAttacks) - self._test_attacks(charged_attacks, ChargedAttacks) - - def _test_attacks(self, callback, clazz): - charged = clazz is ChargedAttacks - self.assertIs(callback(), clazz) - - # check consistency - attacks = clazz.all_by_dps() - number = len(attacks) - self.assertTrue(number > 0) - self.assertGreaterEqual(len(clazz.BY_TYPE), 17) - self.assertEqual(number, len(clazz.all())) - self.assertEqual(number, len(clazz.STATIC_DATA)) - self.assertEqual(number, len(clazz.BY_NAME)) - self.assertEqual(number, sum([len(l) for l in clazz.BY_TYPE.values()])) - - # check data - prev_dps = float("inf") - for attack in attacks: # type: Attack - self.assertGreater(attack.id, 0) - self.assertGreater(len(attack.name), 0) - self.assertGreater(len(attack.type), 0) - self.assertGreaterEqual(attack.damage, 0) - self.assertGreater(attack.duration, .0) - self.assertGreater(attack.energy, 0) - self.assertGreaterEqual(attack.dps, 0) - self.assertTrue(.0 <= attack.rate_in_type <= 1.0) - self.assertLessEqual(attack.dps, prev_dps) - self.assertEqual(attack.is_charged, charged) - self.assertIs(attack, clazz.data_for(attack.id)) - self.assertIs(attack, clazz.by_name(attack.name)) - self.assertTrue(attack in clazz.BY_TYPE[attack.type]) - self.assertIsInstance(attack, ChargedAttack if charged else Attack) - prev_dps = attack.dps