From b8af467f8e694adf3e2353876e27159d4f16daee Mon Sep 17 00:00:00 2001 From: Brice Date: Fri, 12 Aug 2016 00:08:15 +0200 Subject: [PATCH] Improved item recycling (#2482) * Now recycling only if less than 5 space left in inventory Now trying to recycle before moving to/spinning fort if bags are almost full Refactored recycle_items * Removed recycling before moving to/spinning fort if bags are almost full * Removed recycling before moving to/spinning fort if bags are almost full * Removed unused import * Now recycling only if less than 5 space left in inventory Now trying to recycle before moving to/spinning fort if bags are almost full Refactored recycle_items * Removed recycling before moving to/spinning fort if bags are almost full * Added documentation Replace all `logger.log` calls with events! (#2173) * Deleted change on files not concerned * Deleted change on files not concerned * The inner class is now "private" * new class to centralize inventory management * use new inventory class in evolve_pokemon * use new inventory to display # candy after catch * Now using the new inventory (#2528) * Fixed #3256 * Merge branch 'dev' of https://github.com/PokemonGoF/PokemonGo-Bot into PokemonGoF-dev # Conflicts: # pokemongo_bot/cell_workers/recycle_items.py Added methods in the inventory manager needed for the recycle_items task * Fixed error if item_count result is false * Now keeps track of item inventory * Moved inventory update in request_recycle method * Minor comment change * Fixed not running if had more item than inventory size (#3531) * Added to the inventory class the necessary to keep trace of items * Now using the new inventory class properly * Decoupled when to recycle an item from how to do it. * Moved the recycler in the services folder --- pokemongo_bot/cell_workers/recycle_items.py | 168 +++++++++++------- pokemongo_bot/inventory.py | 65 ++++++- pokemongo_bot/services/__init__.py | 0 pokemongo_bot/services/item_recycle_worker.py | 109 ++++++++++++ 4 files changed, 277 insertions(+), 65 deletions(-) create mode 100644 pokemongo_bot/services/__init__.py create mode 100644 pokemongo_bot/services/item_recycle_worker.py diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 3232870d03..4b6ea8c3ca 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -1,85 +1,129 @@ import json import os + +from pokemongo_bot import inventory from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.services.item_recycle_worker import ItemRecycler from pokemongo_bot.tree_config_builder import ConfigException +from pokemongo_bot.worker_result import WorkerResult +DEFAULT_MIN_EMPTY_SPACE = 6 class RecycleItems(BaseTask): SUPPORTED_TASK_API_VERSION = 1 + """ + Recycle undesired items if there is less than five space in inventory. + You can use either item's name or id. For the full list of items see ../../data/items.json + + It's highly recommended to put this task before move_to_fort and spin_fort task in the config file so you'll most likely be able to loot. + + Example config : + { + "type": "RecycleItems", + "config": { + "min_empty_space": 6, # 6 by default + "item_filter": { + "Pokeball": {"keep": 20}, + "Greatball": {"keep": 50}, + "Ultraball": {"keep": 100}, + "Potion": {"keep": 0}, + "Super Potion": {"keep": 0}, + "Hyper Potion": {"keep": 20}, + "Max Potion": {"keep": 50}, + "Revive": {"keep": 0}, + "Max Revive": {"keep": 20}, + "Razz Berry": {"keep": 20} + } + } + } + """ + def initialize(self): + self.items_filter = self.config.get('item_filter', {}) self.min_empty_space = self.config.get('min_empty_space', None) - self.item_filter = self.config.get('item_filter', {}) self._validate_item_filter() def _validate_item_filter(self): + """ + Validate user's item filter config + :return: Nothing. + :rtype: None + :raise: ConfigException: When an item doesn't exist in ../../data/items.json + """ item_list = json.load(open(os.path.join(_base_dir, 'data', 'items.json'))) - for config_item_name, bag_count in self.item_filter.iteritems(): + for config_item_name, bag_count in self.items_filter.iteritems(): if config_item_name not in item_list.viewvalues(): if config_item_name not in item_list: raise ConfigException( "item {} does not exist, spelling mistake? (check for valid item names in data/items.json)".format( config_item_name)) + def should_run(self): + """ + Returns a value indicating whether the recycling process should be run. + :return: True if the recycling process should be run; otherwise, False. + :rtype: bool + """ + if inventory.items().get_space_left() < (DEFAULT_MIN_EMPTY_SPACE if self.min_empty_space is None else self.min_empty_space): + return True + return False + def work(self): - items_in_bag = self.bot.get_inventory_count('item') - total_bag_space = self.bot.player_data['max_item_storage'] - free_bag_space = total_bag_space - items_in_bag - - if self.min_empty_space is not None: - if free_bag_space >= self.min_empty_space and items_in_bag < total_bag_space: - self.emit_event( - 'item_discard_skipped', - formatted="Skipping Recycling of Items. {space} space left in bag.", - data={ - 'space': free_bag_space - } - ) - return - - self.bot.latest_inventory = None - item_count_dict = self.bot.item_inventory_count('all') - - for item_id, bag_count in item_count_dict.iteritems(): - item_name = self.bot.item_list[str(item_id)] - id_filter = self.item_filter.get(item_name, 0) - if id_filter is not 0: - id_filter_keep = id_filter.get('keep', 20) - else: - id_filter = self.item_filter.get(str(item_id), 0) - if id_filter is not 0: - id_filter_keep = id_filter.get('keep', 20) - - bag_count = self.bot.item_inventory_count(item_id) - if (item_name in self.item_filter or str(item_id) in self.item_filter) and bag_count > id_filter_keep: - items_recycle_count = bag_count - id_filter_keep - response_dict_recycle = self.send_recycle_item_request(item_id=item_id, count=items_recycle_count) - result = response_dict_recycle.get('responses', {}).get('RECYCLE_INVENTORY_ITEM', {}).get('result', 0) - - if result == 1: # Request success - self.emit_event( - 'item_discarded', - formatted='Discarded {amount}x {item} (maximum {maximum}).', - data={ - 'amount': str(items_recycle_count), - 'item': item_name, - 'maximum': str(id_filter_keep) - } - ) - else: - self.emit_event( - 'item_discard_fail', - formatted="Failed to discard {item}", - data={ - 'item': item_name - } - ) - - def send_recycle_item_request(self, item_id, count): - # Example of good request response - # {'responses': {'RECYCLE_INVENTORY_ITEM': {'result': 1, 'new_count': 46}}, 'status_code': 1, 'auth_ticket': {'expire_timestamp_ms': 1469306228058L, 'start': '/HycFyfrT4t2yB2Ij+yoi+on778aymMgxY6RQgvrGAfQlNzRuIjpcnDd5dAxmfoTqDQrbz1m2dGqAIhJ+eFapg==', 'end': 'f5NOZ95a843tgzprJo4W7Q=='}, 'request_id': 8145806132888207460L} - return self.bot.api.recycle_inventory_item( - item_id=item_id, - count=count - ) + """ + Discard items if necessary. + :return: Returns wether or not the task went well + :rtype: WorkerResult + """ + # TODO: Use new inventory everywhere and then remove the inventory update + # Updating inventory + inventory.refresh_inventory() + worker_result = WorkerResult.SUCCESS + if self.should_run(): + + # For each user's item in inventory recycle it if needed + for item_in_inventory in inventory.items().all(): + amount_to_recycle = self.get_amount_to_recycle(item_in_inventory) + + if self.item_should_be_recycled(item_in_inventory, amount_to_recycle): + if ItemRecycler(self.bot, item_in_inventory, amount_to_recycle).work() == WorkerResult.ERROR: + worker_result = WorkerResult.ERROR + + return worker_result + + def item_should_be_recycled(self, item, amount_to_recycle): + """ + Returns a value indicating whether the item should be recycled. + :param amount_to_recycle: + :param item: + :return: True if the title should be recycled; otherwise, False. + :rtype: bool + """ + return (item.name in self.items_filter or str( + item.id) in self.items_filter) and amount_to_recycle > 0 + + def get_amount_to_recycle(self, item): + """ + Determine the amount to recycle accordingly to user config + :param item: Item to determine the amount to recycle + :return: The amount to recycle + :rtype: int + """ + amount_to_keep = self.get_amount_to_keep(item) + return 0 if amount_to_keep is None else item.count - amount_to_keep + + def get_amount_to_keep(self, item): + """ + Determine item's amount to keep in inventory. + :param item: + :return: Item's amount to keep in inventory. + :rtype: int + """ + item_filter_config = self.items_filter.get(item.name, 0) + if item_filter_config is not 0: + return item_filter_config.get('keep', 20) + else: + item_filter_config = self.items_filter.get(str(item.id), 0) + if item_filter_config is not 0: + return item_filter_config.get('keep', 20) diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index d7f890933f..3ade99c0af 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1,7 +1,6 @@ import json import logging import os - from pokemongo_bot.base_dir import _base_dir ''' @@ -101,14 +100,59 @@ def captured(self, pokemon_id): return False return self._data[pokemon_id]['times_captured'] > 0 +class Item(object): + def __init__(self, item_id, item_count): + self.id = item_id + self.name = Items.name_for(self.id) + self.count = item_count + + def remove(self, amount): + if self.count < amount: + raise Exception('Tried to remove more {} than you have'.format(self.name)) + self.count -= amount + + def add(self, amount): + if amount < 0: + raise Exception('Must add positive amount of {}'.format(self.name)) + self.count += amount + class Items(_BaseInventoryComponent): TYPE = 'item' ID_FIELD = 'item_id' STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'items.json') - def count_for(self, item_id): - return self._data[item_id]['count'] + def parse(self, item_data): + item_id = item_data.get(Items.ID_FIELD, None) + item_count = item_data['count'] if 'count' in item_data else 0 + return Item(item_id, item_count) + + def get(self, item_id): + return self._data.setdefault(item_id, Item(item_id, 0)) + + @classmethod + def name_for(cls, item_id): + return cls.STATIC_DATA[str(item_id)] + + def get_space_used(self): + """ + Counts the space used in item inventory. + :return: The space used in item inventory. + :rtype: int + """ + space_used = 1 + for item_in_inventory in _inventory.items.all(): + space_used += item_in_inventory.count + return space_used + + def get_space_left(self): + """ + Compute the space left in item inventory. + :return: The space left in item inventory. + :rtype: int + """ + _inventory.retrieve_item_inventory_size() + return _inventory.item_inventory_size - self.get_space_used() class Pokemons(_BaseInventoryComponent): @@ -749,6 +793,7 @@ def __init__(self, bot): self.items = Items() self.pokemons = Pokemons() self.refresh() + self.item_inventory_size = None def refresh(self): # TODO: it would be better if this class was used for all @@ -761,6 +806,16 @@ def refresh(self): user_web_inventory = os.path.join(_base_dir, 'web', 'inventory-%s.json' % (self.bot.config.username)) with open(user_web_inventory, 'w') as outfile: json.dump(inventory, outfile) + def retrieve_item_inventory_size(self): + """ + Retrieves the item inventory size + :return: Nothing. + :rtype: None + """ + # TODO: Force update of _item_inventory_size if the player upgrades its size + if self.item_inventory_size is None: + self.item_inventory_size = self.bot.api.get_player()['responses']['GET_PLAYER']['player_data']['max_item_storage'] + # # Usage helpers @@ -768,6 +823,7 @@ def refresh(self): # STAB (Same-type attack bonus) STAB_FACTOR = 1.25 + _inventory = None LevelToCPm() # init LevelToCPm FastAttacks() # init FastAttacks @@ -812,6 +868,9 @@ def init_inventory(bot): def refresh_inventory(): _inventory.refresh() +def get_item_inventory_size(): + _inventory.retrieve_item_inventory_size() + return _inventory.item_inventory_size def pokedex(): return _inventory.pokedex diff --git a/pokemongo_bot/services/__init__.py b/pokemongo_bot/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pokemongo_bot/services/item_recycle_worker.py b/pokemongo_bot/services/item_recycle_worker.py new file mode 100644 index 0000000000..e59270d4ea --- /dev/null +++ b/pokemongo_bot/services/item_recycle_worker.py @@ -0,0 +1,109 @@ +from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.base_task import BaseTask +from pokemongo_bot import inventory +from pokemongo_bot.tree_config_builder import ConfigException + +RECYCLE_REQUEST_RESPONSE_SUCCESS = 1 +class ItemRecycler(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + """ + This class contains details of recycling process. + """ + def __init__(self, bot, item_to_recycle, amount_to_recycle): + """ + Initialise an instance of ItemRecycler + :param bot: The instance of the Bot + :param item_to_recycle: The item to recycle + :type item_to_recycle: Item + :param amount_to_recycle: The amount to recycle + :type amount_to_recycle: int + :return: Nothing. + :rtype: None + """ + self.bot = bot + self.item_to_recycle = item_to_recycle + self.amount_to_recycle = amount_to_recycle + self.recycle_item_request_result = None + + def work(self): + """ + Recycle an item + :return: Returns wether or not the task went well + :rtype: WorkerResult + """ + if self.should_run(): + self.request_recycle() + if self.is_recycling_success(): + self._update_inventory() + self._emit_recycle_succeed() + return WorkerResult.SUCCESS + else: + self._emit_recycle_failed() + return WorkerResult.ERROR + + def should_run(self): + """ + Returns a value indicating whether or mot the recycler should be run. + :return: True if the recycler should be run; otherwise, False. + :rtype: bool + """ + if self.amount_to_recycle > 0 and self.item_to_recycle is not None: + return True + return False + + def request_recycle(self): + """ + Request recycling of the item and store api call response's result. + :return: Nothing. + :rtype: None + """ + response = self.bot.api.recycle_inventory_item(item_id=self.item_to_recycle.id, + count=self.amount_to_recycle) + # Example of good request response + # {'responses': {'RECYCLE_INVENTORY_ITEM': {'result': 1, 'new_count': 46}}, 'status_code': 1, 'auth_ticket': {'expire_timestamp_ms': 1469306228058L, 'start': '/HycFyfrT4t2yB2Ij+yoi+on778aymMgxY6RQgvrGAfQlNzRuIjpcnDd5dAxmfoTqDQrbz1m2dGqAIhJ+eFapg==', 'end': 'f5NOZ95a843tgzprJo4W7Q=='}, 'request_id': 8145806132888207460L} + self.recycle_item_request_result = response.get('responses', {}).get('RECYCLE_INVENTORY_ITEM', {}).get('result', 0) + + def _update_inventory(self): + """ + Updates the inventory. Prevent an unnecessary call to the api + :return: Nothing. + :rtype: None + """ + inventory.items().get(self.item_to_recycle.id).remove(self.amount_to_recycle) + + def is_recycling_success(self): + """ + Returns a value indicating whether or not the item has been successfully recycled. + :return: True if the item has been successfully recycled; otherwise, False. + :rtype: bool + """ + return self.recycle_item_request_result == RECYCLE_REQUEST_RESPONSE_SUCCESS + + def _emit_recycle_succeed(self): + """ + Emits recycle succeed event in logs + :return: Nothing. + :rtype: None + """ + self.emit_event( + 'item_discarded', + formatted='Discarded {amount}x {item}).', + data={ + 'amount': str(self.amount_to_recycle), + 'item': self.item_to_recycle.name, + } + ) + + def _emit_recycle_failed(self): + """ + Emits recycle failed event in logs + :return: Nothing. + :rtype: None + """ + self.emit_event( + 'item_discard_fail', + formatted="Failed to discard {item}", + data={ + 'item': self.item_to_recycle.name + } + )