From 03d7f92ebf127572337d5fab748a4bde1577a186 Mon Sep 17 00:00:00 2001 From: aeckert Date: Tue, 9 Aug 2016 02:18:31 -0600 Subject: [PATCH] [Inventory Management] Add a central class for caching/parsing inventory & static data (#2528) * new class to centralize inventory management * use new inventory class in evolve_pokemon * use new inventory to display # candy after catch --- pokemongo_bot/__init__.py | 7 +- pokemongo_bot/cell_workers/evolve_pokemon.py | 85 ++---- .../cell_workers/pokemon_catch_worker.py | 18 +- pokemongo_bot/inventory.py | 251 ++++++++++++++++++ 4 files changed, 293 insertions(+), 68 deletions(-) create mode 100644 pokemongo_bot/inventory.py diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 4ea6ac3b15..711ddd1721 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -30,8 +30,11 @@ from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl from worker_result import WorkerResult from tree_config_builder import ConfigException, MismatchTaskApiVersion, TreeConfigBuilder +from inventory import init_inventory from sys import platform as _platform import struct + + class PokemonGoBot(object): @property def position(self): @@ -286,7 +289,7 @@ def _register_events(self): self.event_manager.register_event('skip_evolve') self.event_manager.register_event('threw_berry_failed', parameters=('status_code',)) self.event_manager.register_event('vip_pokemon') - + self.event_manager.register_event('gained_candy', parameters=('quantity', 'type')) # level up stuff self.event_manager.register_event( @@ -782,6 +785,8 @@ def get_inventory(self): return self.latest_inventory def update_inventory(self): + # TODO: transition to using this inventory class everywhere + init_inventory(self) response = self.get_inventory() self.inventory = list() inventory_items = response.get('responses', {}).get('GET_INVENTORY', {}).get( diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index c3903a685d..d045fc8fef 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -1,3 +1,4 @@ +from pokemongo_bot import inventory from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.item_list import Item from pokemongo_bot.base_task import BaseTask @@ -25,21 +26,16 @@ def work(self): if not self._should_run(): return - response_dict = self.api.get_inventory() - inventory_items = response_dict.get('responses', {}).get('GET_INVENTORY', {}).get('inventory_delta', {}).get( - 'inventory_items', {}) - - evolve_list = self._sort_and_filter(inventory_items) + evolve_list = self._sort_and_filter() if self.evolve_all[0] != 'all': # filter out non-listed pokemons - evolve_list = filter(lambda x: x["name"] in self.evolve_all, evolve_list) + evolve_list = filter(lambda x: x.name in self.evolve_all, evolve_list) cache = {} - candy_list = self._get_candy_list(inventory_items) for pokemon in evolve_list: - if self._can_evolve(pokemon, candy_list, cache): - self._execute_pokemon_evolve(pokemon, candy_list, cache) + if pokemon.can_evolve_now(): + self._execute_pokemon_evolve(pokemon, cache) def _should_run(self): if not self.evolve_all or self.evolve_all[0] == 'none': @@ -80,81 +76,40 @@ def _should_run(self): ) return False - def _get_candy_list(self, inventory_items): - candies = {} - for item in inventory_items: - candy = item.get('inventory_item_data', {}).get('candy', {}) - family_id = candy.get('family_id', 0) - amount = candy.get('candy', 0) - if family_id > 0 and amount > 0: - family = self.bot.pokemon_list[family_id - 1]['Name'] + " candies" - candies[family] = amount - - return candies - - def _sort_and_filter(self, inventory_items): + def _sort_and_filter(self): pokemons = [] logic_to_function = { - 'or': lambda pokemon: pokemon["cp"] >= self.evolve_above_cp or pokemon["iv"] >= self.evolve_above_iv, - 'and': lambda pokemon: pokemon["cp"] >= self.evolve_above_cp and pokemon["iv"] >= self.evolve_above_iv + 'or': lambda pokemon: pokemon.cp >= self.evolve_above_cp or pokemon.iv >= self.evolve_above_iv, + 'and': lambda pokemon: pokemon.cp >= self.evolve_above_cp and pokemon.iv >= self.evolve_above_iv } - for item in inventory_items: - pokemon = item.get('inventory_item_data', {}).get('pokemon_data', {}) - pokemon_num = int(pokemon.get('pokemon_id', 0)) - 1 - next_evol = self.bot.pokemon_list[pokemon_num].get('Next Evolution Requirements', {}) - pokemon = { - 'id': pokemon.get('id', 0), - 'num': pokemon_num, - 'name': self.bot.pokemon_list[pokemon_num]['Name'], - 'cp': pokemon.get('cp', 0), - 'iv': self._compute_iv(pokemon), - 'candies_family': next_evol.get('Name', ""), - 'candies_amount': next_evol.get('Amount', 0) - } - if pokemon["id"] > 0 and pokemon["candies_amount"] > 0 and (logic_to_function[self.cp_iv_logic](pokemon)): + + for pokemon in inventory.pokemons().all(): + if pokemon.id > 0 and pokemon.has_next_evolution() and (logic_to_function[self.cp_iv_logic](pokemon)): pokemons.append(pokemon) if self.first_evolve_by == "cp": - pokemons.sort(key=lambda x: (x['num'], x["cp"], x["iv"]), reverse=True) + pokemons.sort(key=lambda x: (x.pokemon_id, x.cp, x.iv), reverse=True) else: - pokemons.sort(key=lambda x: (x['num'], x["iv"], x["cp"]), reverse=True) + pokemons.sort(key=lambda x: (x.pokemon_id, x.iv, x.cp), reverse=True) return pokemons - def _can_evolve(self, pokemon, candy_list, cache): - - if pokemon["name"] in cache: - return False - - family = pokemon["candies_family"] - amount = pokemon["candies_amount"] - if family in candy_list and candy_list[family] >= amount: - return True - else: - cache[pokemon["name"]] = 1 - return False - - def _execute_pokemon_evolve(self, pokemon, candy_list, cache): - pokemon_id = pokemon["id"] - pokemon_name = pokemon["name"] - pokemon_cp = pokemon["cp"] - pokemon_iv = pokemon["iv"] - - if pokemon_name in cache: + def _execute_pokemon_evolve(self, pokemon, cache): + if pokemon.name in cache: return False - response_dict = self.api.evolve_pokemon(pokemon_id=pokemon_id) + response_dict = self.api.evolve_pokemon(pokemon_id=pokemon.id) if response_dict.get('responses', {}).get('EVOLVE_POKEMON', {}).get('result', 0) == 1: self.emit_event( 'pokemon_evolved', formatted="Successfully evolved {pokemon} with CP {cp} and IV {iv}!", data={ - 'pokemon': pokemon_name, - 'iv': pokemon_iv, - 'cp': pokemon_cp + 'pokemon': pokemon.name, + 'iv': pokemon.iv, + 'cp': pokemon.cp } ) - candy_list[pokemon["candies_family"]] -= pokemon["candies_amount"] + inventory.candies().get(pokemon.pokemon_id).consume(pokemon.evolution_cost) sleep(self.evolve_speed) return True else: diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index ae55b7d704..207ed83d81 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import time +from pokemongo_bot import inventory from pokemongo_bot.base_task import BaseTask from pokemongo_bot.human_behaviour import normalized_reticle_size, sleep, spin_modifier @@ -27,8 +28,8 @@ class Pokemon(object): def __init__(self, pokemon_list, pokemon_data): - self.num = int(pokemon_data['pokemon_id']) - 1 - self.name = pokemon_list[int(self.num)]['Name'] + self.num = int(pokemon_data['pokemon_id']) + self.name = pokemon_list[int(self.num) - 1]['Name'] self.cp = pokemon_data['cp'] self.attack = pokemon_data.get('individual_attack', 0) self.defense = pokemon_data.get('individual_defense', 0) @@ -388,6 +389,19 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) } ) + + # We could refresh here too, but adding 3 saves a inventory request + candy = inventory.candies().get(pokemon.num) + candy.add(3) + self.emit_event( + 'gained_candy', + formatted='You now have {quantity} {type} candy!', + data = { + 'quantity': candy.quantity, + 'type': candy.type, + }, + ) + self.bot.softban = False # evolve pokemon if necessary diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py new file mode 100644 index 0000000000..1cd8a893b2 --- /dev/null +++ b/pokemongo_bot/inventory.py @@ -0,0 +1,251 @@ +import json +import os + +''' +Helper class for updating/retrieving Inventory data +''' + +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 + + 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 = json.load(open(cls.STATIC_DATA_FILE)) + + def parse(self, item): + # optional hook for parsing the dict for this item + # default is to use the dict directly + return item + + def retrieve_data(self, inventory): + assert self.TYPE is not None + assert self.ID_FIELD is not None + ret = {} + for item in inventory: + data = item['inventory_item_data'] + if self.TYPE in data: + item = data[self.TYPE] + key = item[self.ID_FIELD] + ret[key] = self.parse(item) + return ret + + def refresh(self, inventory): + self._data = self.retrieve_data(inventory) + + def get(self, id): + return self._data(id) + + def all(self): + return list(self._data.values()) + + +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(self, pokemon_id): + return Pokemons.first_evolution_id_for(pokemon_id) + + def get(self, pokemon_id): + family_id = self.family_id_for(pokemon_id) + return self._data.setdefault(family_id, Candy(family_id, 0)) + + def parse(self, item): + candy = item['candy'] if 'candy' in item else 0 + return Candy(item['family_id'], candy) + + +class Pokedex(_BaseInventoryComponent): + TYPE = 'pokedex_entry' + ID_FIELD = 'pokemon_id' + + def seen(self, pokemon_id): + return pokemon_id in self._data + + def captured(self, pokemon_id): + if not self.seen(pokemon_id): + return False + return self._data[pokemon_id]['times_captured'] > 0 + + +class Items(_BaseInventoryComponent): + TYPE = 'item' + ID_FIELD = 'item_id' + STATIC_DATA_FILE = os.path.join('data', 'items.json') + + def count_for(self, item_id): + return self._data[item_id]['count'] + + +class Pokemons(_BaseInventoryComponent): + TYPE = 'pokemon_data' + ID_FIELD = 'id' + STATIC_DATA_FILE = os.path.join('data', 'pokemon.json') + + def parse(self, item): + if 'is_egg' in item: + return Egg(item) + return Pokemon(item) + + @classmethod + def data_for(cls, pokemon_id): + return cls.STATIC_DATA[pokemon_id - 1] + + @classmethod + def name_for(cls, pokemon_id): + return cls.data_for(pokemon_id)['Name'] + + @classmethod + def first_evolution_id_for(cls, pokemon_id): + data = cls.data_for(pokemon_id) + if 'Previous evolution(s)' in data: + return int(data['Previous evolution(s)'][0]['Number']) + return pokemon_id + + @classmethod + def next_evolution_id_for(cls, pokemon_id): + try: + return int(cls.data_for(pokemon_id)['Next evolution(s)'][0]['Number']) + except KeyError: + return None + + @classmethod + def evolution_cost_for(cls, pokemon_id): + try: + return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + except KeyError: + 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)] + +class Egg(object): + def __init__(self, data): + self._data = data + + def has_next_evolution(self): + return False + + +class Pokemon(object): + def __init__(self, data): + self._data = data + self.id = data['id'] + self.pokemon_id = data['pokemon_id'] + self.cp = data['cp'] + self._static_data = Pokemons.data_for(self.pokemon_id) + self.name = Pokemons.name_for(self.pokemon_id) + self.iv = self._compute_iv() + + def can_evolve_now(self): + return self.has_next_evolution and self.candy_quantity > self.evolution_cost + + def has_next_evolution(self): + return 'Next Evolution Requirements' in self._static_data + + def has_seen_next_evolution(self): + return pokedex().captured(self.next_evolution_id) + + @property + 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 candy_quantity(self): + return candies().get(self.pokemon_id).quantity + + @property + def evolution_cost(self): + return self._static_data['Next Evolution Requirements']['Amount'] + + def _compute_iv(self): + total_IV = 0.0 + iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] + + 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): + def __init__(self, bot): + self.bot = bot + self.pokedex = Pokedex() + self.candy = Candies() + self.items = Items() + self.pokemons = Pokemons() + self.refresh() + + def refresh(self): + # TODO: it would be better if this class was used for all + # inventory management. For now, I'm just clearing the old inventory field + self.bot.latest_inventory = None + inventory = self.bot.get_inventory()['responses']['GET_INVENTORY'][ + 'inventory_delta']['inventory_items'] + for i in (self.pokedex, self.candy, self.items, self.pokemons): + i.refresh(inventory) + + +_inventory = None + +def init_inventory(bot): + global _inventory + _inventory = Inventory(bot) + + +def refresh_inventory(): + _inventory.refresh() + + +def pokedex(): + return _inventory.pokedex + + +def candies(refresh=False): + if refresh: + refresh_inventory() + return _inventory.candy + + +def pokemons(): + return _inventory.pokemons + + +def items(): + return _inventory.items