Source code for scriptabit.plugins.pet_care

# -*- coding: utf-8 -*-
""" Habitica pet care.

Options for batch hatching and feeding pets.
"""
# Ensure backwards compatibility with Python 2
from __future__ import (
    absolute_import,
    division,
    print_function,
    unicode_literals)
from builtins import *
import logging
import random
from pprint import pprint
from time import sleep

import scriptabit


[docs]class PetCare(scriptabit.IPlugin): """ Habitica pet care """
[docs] def __init__(self): """ Initialises the plugin. """ super().__init__() self.__items = None self.__any_food = False # Generate the reference lists self.__base_pets = [ 'BearCub', 'Cactus', 'Dragon', 'FlyingPig', 'Fox', 'LionCub', 'PandaCub', 'TigerCub', 'Wolf', ] self.__rare_pets = [ 'Wolf-Veteran', 'Mammoth-Base', 'JackOLantern-Base', 'Turkey-Base', 'BearCub-Polar', ] self.__preferred_foods = { 'Base': ['Meat'], 'CottonCandyBlue': ['CottonCandyBlue'], 'CottonCandyPink': ['CottonCandyPink'], 'Desert': ['Potatoe'], 'Golden': ['Honey'], 'Red': ['Strawberry'], 'Skeleton': ['Fish'], 'White': ['Milk'], 'Zombie': ['RottenMeat'], 'Shade': ['Chocolate'], } self.__base_potions = [ 'Base', 'White', 'Desert', 'Red', 'Shade', 'Skeleton', 'Zombie', 'CottonCandyPink', 'CottonCandyBlue', 'Golden', ] # augment the preferred foods with the special foods for potion in self.__base_potions: self.__preferred_foods[potion].append('Cake_{0}'.format(potion)) self.__preferred_foods[potion].append('Candy_{0}'.format(potion)) # build a list of all foods, for the special potions self.__all_foods = [] for f in self.__preferred_foods.values(): self.__all_foods.extend(f)
[docs] def get_arg_parser(self): """Gets the argument parser containing any CLI arguments for the plugin. Returns: argparse.ArgParser: The `ArgParser` containing the argument definitions. """ parser = super().get_arg_parser() parser.add( '--list-pets', required=False, action='store_true', help='Lists all pet-related items') parser.add( '--feed-pets', required=False, action='store_true', help='Batch pet feeding') parser.add( '--hatch-pets', required=False, action='store_true', help='Batch pet hatching') parser.add( '--any-pet-food', required=False, action='store_true', help='When feeding pets, allows the use of non-preferred food') parser.add( '--no-base-pets', required=False, action='store_true', help='Disables feeding of base pets') parser.add( '--quest-pets', required=False, action='store_true', help='Allows feeding of quest pets') parser.add( '--magic-pets', required=False, action='store_true', help='Allows feeding of magic pets') parser.add( '--no-raise', required=False, action='store_true', help='When feeding pets, this flag prevents them being raised to mounts') self.print_help = parser.print_help return parser
[docs] def initialise(self, configuration, habitica_service, data_dir): """ Initialises the plugin. Generally, any initialisation should be done here rather than in activate or __init__. Args: configuration (ArgParse.Namespace): The application configuration. habitica_service: the Habitica Service instance. data_dir (str): A writeable directory that the plugin can use for persistent data. """ super().initialise(configuration, habitica_service, data_dir) logging.getLogger(__name__).info('Scriptabit Pet Care Services: looking' ' after your pets since yesterday') self.__items = self._hs.get_user()['items'] self.__any_food = self._config.any_pet_food
[docs] @staticmethod def supports_dry_runs(): """ The PetCare plugin supports dry runs. Returns: bool: True """ return True
[docs] def update_interval_minutes(self): """ Indicates the required update interval in minutes. Returns: float: The required update interval in minutes. """ return 30
[docs] def update(self): """ This update method will be called once on every update cycle, with the frequency determined by the value returned from `update_interval_minutes()`. If a plugin implements a single-shot function, then update should return `False`. Returns: bool: True if further updates are required; False if the plugin is finished and the application should shut down. """ # do work here if self._config.list_pets: self.list_pet_items(self.__items) return False if self._config.feed_pets: self.feed_pets() return False if self._config.hatch_pets: self.hatch_pets() return False # if no other options selected, print plugin specific help and exit self.print_help() # return False if finished, and True to be updated again. return False
[docs] def is_quest_egg(self, egg): """ Is this a quest egg? In the current API, magic and base pets share the same core set of eggs, so anything not in that list is assumed to be a quest egg. Args: egg (str): The egg name. """ return egg not in self.__base_pets
[docs] def is_magic_potion(self, potion): """ Is this a magic potion? In the current API, any potion not in the core list of standard potions is a magic potion. Args: potion (str): The potion name. """ return potion not in self.__base_potions
[docs] def is_base_pet(self, pet): """ Is this a base pet? Args: pet (str): The full pet name. """ animal, potion = pet.split('-') return animal in self.__base_pets and potion in self.__base_potions
[docs] def is_quest_pet(self, pet): """ Is this a quest pet? Args: pet (str): The full pet name. """ return not (self.is_base_pet(pet) or self.is_magic_pet(pet) or self.is_rare_pet(pet))
[docs] def is_magic_pet(self, pet): """ Is this a magic pet? Args: pet (str): The full pet name. """ animal, potion = pet.split('-') return not self.is_rare_pet(pet) and \ potion not in self.__base_potions
[docs] def is_rare_pet(self, pet): """ Is this a rare pet? Args: pet (str): The full pet name. """ return pet in self.__rare_pets
[docs] def get_eggs(self, base=True, quest=False): """ Gets the filtered dictionary of available eggs. Values indicate current quantity. Args: base (bool): Includes or excludes standard eggs. eggs (bool): Includes or excludes quest eggs. Returns: dict: The dictionary of eggs and quantities. """ eggs = {} for e, q in self.__items['eggs'].items(): is_base_egg = e in self.__base_pets if base and is_base_egg: eggs[e] = q elif quest and not is_base_egg: eggs[e] = q return eggs
[docs] def get_hatching_potions(self, base=True, magic=False): """ Gets the filtered dictionary of available hatching potions. Values indicate current quantity. Args: base (bool): Includes or excludes standard potions. magic (bool): Includes or excludes magic potions. Returns: dict: The dictionary of potions and quantities. """ hp = {} for p, q in self.__items['hatchingPotions'].items(): if (base and p in self.__base_potions) or\ (magic and p not in self.__base_potions): hp[p] = q return hp
[docs] def get_pets( self, base=True, magic=False, quest=False, rare=False, feedable_only=False): """ Gets a filtered list of current user pets. Args: base (bool): Includes or excludes base pets. magic (bool): Includes or excludes magic pets. quest (bool): Includes or excludes quest pets. rare (bool): Includes or excludes rare pets. feedable_only (bool): If true, only feedable pets are included. Pets where a matching mount exists are not feedable. Returns: list: the filtered pet list. """ pets = [] for pet, growth in self.__items['pets'].items(): # Habitica indicates a pet that has been raised to a mount with # growth == -1. These pets are non-interactive, so exclude them. # There is no direct indication of the second pet (where a mount # also exists), so we check for the presence of a mount. if growth > 0: has_mount = self.__items['mounts'].get(pet, False) # if the no-raise flag is true, then we skip pets that are near # to being raised to mounts if self._config.no_raise and growth >= 45: continue if not (feedable_only and has_mount): if base and self.is_base_pet(pet): pets.append(pet) elif magic and self.is_magic_pet(pet): pets.append(pet) elif quest and self.is_quest_pet(pet): pets.append(pet) elif rare and self.is_rare_pet(pet): pets.append(pet) return pets
[docs] def feed_pets(self): """ Feeds all current pets. """ pets = self.get_pets( base=not self._config.no_base_pets, magic=self._config.magic_pets, quest=self._config.quest_pets, rare=False, feedable_only=True) pet_count = 0 food_count = 0 mounts_raised = 0 for pet in pets: if not self.has_any_food(): logging.getLogger(__name__).info('Out of food') break try: food = self.get_food_for_pet(pet) fed_something = False while food: fed_something = True if self.dry_run: response = {'data': -1, 'message': 'dry run'} else: response = self._hs.feed_pet(pet, food) food_count += 1 self.consume_food(food) growth = response['data'] logging.getLogger(__name__).info( '%s (%d): %s', pet, growth, response['message']) # Doh: this test needs to be first, or it gets masked by the # growth > 0 test. if self._config.no_raise and growth >= 45: break elif growth > 0: # growth > 0 indicates that the pet is still hungry food = self.get_food_for_pet(pet) else: # growth <= 0 (-1 actually) indicates that the # pet became a mount mounts_raised += 1 break if fed_something and not self.dry_run: sleep(2) # sleep for a bit so we don't pound the server except Exception as e: logging.getLogger(__name__).warning(e) pet_count += 1 message = \ 'Checked {1} pets, fed {0} pieces of food, raised {2} mounts'.\ format(food_count, pet_count, mounts_raised) self.notify(message)
[docs] def get_food_for_pet(self, pet): """ Gets a food item for a pet Args: pet (str): The composite pet name (animal-potion) Returns: str: The name of a suitable food if that food is in stock. If no suitable food is available, then None is returned. """ # split the pet name animal, potion = pet.split('-') # magic pets eat any food if self.__any_food or self.is_magic_pet(pet, animal, potion): for food, quantity in self.__items['food'].items(): if quantity > 0: return food else: for food in self.__preferred_foods[potion]: if self.has_food(food): return food return None
[docs] def has_any_food(self): """ Checks whether any food is left. Returns: bool: True if some food remains, otherwise False. """ return sum(self.__items['food'].values()) > 0
[docs] def has_food(self, food): """ Returns True if the food is in stock, otherwise False. Args: food (str): The food to check Returns: bool: True if the food is in stock, otherwise False. """ return self.__items['food'].get(food, 0) > 0
[docs] def consume_food(self, food): """ Consumes a food, updating the cached quantity. Args: food (str): The food. """ quantity = self.__items['food'].get(food, 0) if quantity > 0: self.__items['food'][food] = quantity - 1
[docs] @staticmethod def list_pet_items(items): """ Lists all pet-related inventory items. Args: items (dict): The Habitica user.items dictionary. """ print() print('Food:') pprint(items['food']) print() print('Eggs:') pprint(items['eggs']) print() print('Hatching potions:') pprint(items['hatchingPotions']) print() print('Pets:') pprint(items['pets']) print() print('Mounts:') pprint(items['mounts'])
[docs] def notify(self, message, **kwargs): """ Notify the Habitica user. If this is a dry run, then the message is logged. Otherwise the message is logged and posted to the Habitica notification panel. Args: message (str): The message. panel (bool): If True, the Habitica panel is updated. """ emoticons = [ 'dog', 'mouse', 'snake', 'snail', 'monkey_face', 'panda_face', 'pig', 'whale', 'dragon', 'monkey', 'wolf', 'bear', 'dragon_face', 'cactus'] super().notify( ':{0}: {1}'.format( random.choice(emoticons), message), **kwargs)
[docs] def hatch_pets(self): """ Hatch all available pets. """ # first get the lists of things current_pets = self.get_pets( base=not self._config.no_base_pets, magic=self._config.magic_pets, quest=self._config.quest_pets, rare=False) potions = self.get_hatching_potions( base=True, # we always need the base potions magic=self._config.magic_pets) eggs = self.get_eggs( base=not self._config.no_base_pets, quest=self._config.quest_pets) hatched = 0 for egg, egg_quantity in eggs.items(): for potion, potion_quantity in potions.items(): potential_pet = '{0}-{1}'.format(egg, potion) if egg_quantity <= 0: # logging.getLogger(__name__).debug( # "Can't hatch %s, no egg", potential_pet) continue if potion_quantity <= 0: # logging.getLogger(__name__).debug( # "Can't hatch %s, no potion", potential_pet) continue if potential_pet in current_pets: # logging.getLogger(__name__).debug( # "Can't hatch %s, already have one", potential_pet) continue if self.is_quest_egg(egg) and self.is_magic_potion(potion): # this is not a valid combination continue try: if self.dry_run: response = {'message': 'dry run'} else: response = self._hs.hatch_pet(egg, potion) logging.getLogger(__name__).info( '%s: %s', potential_pet, response['message']) hatched += 1 potions[potion] -= 1 # don't need to store the egg_quantity back into the dict, # since it is the outer loop egg_quantity -= 1 # strictly speaking don't need this either, since we # shouldn't ever revist the same egg/potion combo current_pets.append(potential_pet) except Exception as e: logging.getLogger(__name__).warning(e) message = 'Hatched {0} new pets'.format(hatched) self.notify(message)