diff --git a/configs/config.json.example b/configs/config.json.example index 3442c233d9..f032fcd97f 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -56,13 +56,17 @@ "config": { "lure_attraction": true, "lure_max_distance": 2000 + } }, { - "type": "FollowSpiral" + "type": "FollowSpiral", + "config": { + "diameter": 4, + "step_size": 70 + } } ], "map_object_cache_time": 5, - "max_steps": 5, "forts": { "avoid_circles": true, "max_circle_size": 50 @@ -78,8 +82,13 @@ "distance_unit": "km", "reconnecting_timeout": 15, "evolve_captured": "NONE", - "catch_randomize_reticle_factor": 1.0, - "catch_randomize_spin_factor": 1.0, + "catch_throw_parameters": { + "excellent_rate": 0.1, + "great_rate": 0.5, + "nice_rate": 0.3, + "normal_rate": 0.1, + "spin_success_rate" : 0.6, + }, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 4d340479c5..b1e6aa939b 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -52,7 +52,11 @@ "type": "SpinFort" }, { - "type": "MoveToFort" + "type": "MoveToFort", + "config":{ + "lure_attraction": true, + "lure_max_distance": 2000 + } }, { "type": "FollowSpiral" diff --git a/pokecli.py b/pokecli.py index d37bc16313..3a2934cebe 100755 --- a/pokecli.py +++ b/pokecli.py @@ -225,16 +225,6 @@ def init_config(): type=str, default=None ) - add_config( - parser, - load, - short_flag="-ms", - long_flag="--max_steps", - help= - "Set the steps around your initial location(DEFAULT 5 mean 25 cells around your location)", - type=int, - default=50 - ) add_config( parser, @@ -311,26 +301,55 @@ def init_config(): add_config( parser, load, - long_flag="--catch_randomize_reticle_factor", - help="Randomize factor for pokeball throwing accuracy (DEFAULT 1.0 means no randomize: always 'Excellent' throw. 0.0 randomizes between normal and 'Excellent' throw)", + long_flag="--map_object_cache_time", + help="Amount of seconds to keep the map object in cache (bypass Niantic throttling)", type=float, - default=1.0 + default=5.0 ) add_config( parser, load, - long_flag="--catch_randomize_spin_factor", - help="Randomize factor for pokeball curve throwing (DEFAULT 1.0 means no randomize: always perfect 'Super Spin' curve ball. 0.0 randomizes between normal and 'Super Spin' curve ball)", + short_flag="-cte", + long_flag="--catch_throw_parameters.excellent_rate", + help="Define the odd of performing an excellent throw", type=float, - default=1.0 + default=1, ) add_config( parser, load, - long_flag="--map_object_cache_time", - help="Amount of seconds to keep the map object in cache (bypass Niantic throttling)", + short_flag="-ctg", + long_flag="--catch_throw_parameters.great_rate", + help="Define the odd of performing a great throw", type=float, - default=5.0 + default=0, + ) + add_config( + parser, + load, + short_flag="-ctn", + long_flag="--catch_throw_parameters.nice_rate", + help="Define the odd of performing a nice throw", + type=float, + default=0, + ) + add_config( + parser, + load, + short_flag="-ctm", + long_flag="--catch_throw_parameters.normal_rate", + help="Define the odd of performing a normal throw", + type=float, + default=0, + ) + add_config( + parser, + load, + short_flag="-cts", + long_flag="--catch_throw_parameters.spin_success_rate", + help="Define the odds of performing a spin throw (Value between 0 (never) and 1 (always))", + type=float, + default=1, ) # Start to parse other attrs @@ -346,6 +365,7 @@ def init_config(): config.action_wait_min = load.get('action_wait_min', 1) config.raw_tasks = load.get('tasks', []) config.vips = load.get('vips',{}) + config.catch_throw_parameters = load.get('catch_throw_parameters', {'excellent_rate': 1.0, 'spin_success_rate': 1.0}) if config.map_object_cache_time < 0.0: parser.error("--map_object_cache_time is out of range! (should be >= 0.0)") @@ -367,7 +387,7 @@ def task_configuration_error(flag_name): """.format(flag_name)) old_flags = ['mode', 'catch_pokemon', 'spin_forts', 'forts_spin', 'hatch_eggs', 'release_pokemon', 'softban_fix', - 'longer_eggs_first', 'evolve_speed', 'use_lucky_egg', 'item_filter', 'evolve_all', 'evolve_cp_min'] + 'longer_eggs_first', 'evolve_speed', 'use_lucky_egg', 'item_filter', 'evolve_all', 'evolve_cp_min', 'max_steps'] for flag in old_flags: if flag in load: task_configuration_error(flag) @@ -390,14 +410,6 @@ def task_configuration_error(flag_name): parser.error("Needs either --use-location-cache or --location.") return None - if config.catch_randomize_reticle_factor < 0 or 1 < config.catch_randomize_reticle_factor: - parser.error("--catch_randomize_reticle_factor is out of range! (should be 0 <= catch_randomize_reticle_factor <= 1)") - return None - - if config.catch_randomize_spin_factor < 0 or 1 < config.catch_randomize_spin_factor: - parser.error("--catch_randomize_spin_factor is out of range! (should be 0 <= catch_randomize_spin_factor <= 1)") - return None - # create web dir if not exists try: os.makedirs(web_dir) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 811fd02245..4c21f87493 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -32,6 +32,10 @@ class PokemonGoBot(object): def position(self): return self.api._position_lat, self.api._position_lng, 0 + @position.setter + def position(self, position_tuple): + self.api._position_lat, self.api._position_lng, self.api._position_alt = position_tuple + def __init__(self, config): self.config = config self.fort_timeouts = dict() @@ -103,7 +107,7 @@ def get_meta_cell(self): wild_pokemons += cell["wild_pokemons"] if "catchable_pokemons" in cell and len(cell["catchable_pokemons"]): catchable_pokemons += cell["catchable_pokemons"] - + # If there are forts present in the cells sent from the server or we don't yet have any cell data, return all data retrieved if len(forts) > 1 or not self.cell: return { @@ -138,14 +142,13 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): if 'forts' in cell: for fort in cell['forts']: if fort.get('type') != 1: - self.api.get_gym_details( + response_gym_details = self.api.get_gym_details( gym_id=fort.get('id'), player_latitude=lng, player_longitude=lat, gym_latitude=fort.get('latitude'), gym_longitude=fort.get('longitude') ) - response_gym_details = self.api.call() fort['gym_details'] = response_gym_details.get( 'responses', {} ).get('GET_GYM_DETAILS', None) @@ -239,6 +242,9 @@ def check_session(self, position): if remaining_time < 60: logger.log("Session stale, re-logging in", 'yellow') + position = self.position + self.api = ApiWrapper() + self.position = position self.login() @staticmethod @@ -251,13 +257,13 @@ def is_numeric(s): def login(self): logger.log('Attempting login to Pokemon Go.', 'white') - self.api.reset_auth() lat, lng = self.position[0:2] self.api.set_position(lat, lng, 0) - while not self.api.login(self.config.auth_service, - str(self.config.username), - str(self.config.password)): + while not self.api.login( + self.config.auth_service, + str(self.config.username), + str(self.config.password)): logger.log('[X] Login Error, server busy', 'red') logger.log('[X] Waiting 10 seconds to try again', 'red') @@ -267,7 +273,7 @@ def login(self): def _setup_api(self): # instantiate pgoapi - self.api = ApiWrapper(PGoApi()) + self.api = ApiWrapper() # provide player position on the earth self._set_starting_position() @@ -286,8 +292,7 @@ def _setup_api(self): def _print_character_info(self): # get player profile call # ---------------------- - self.api.get_player() - response_dict = self.api.call() + response_dict = self.api.get_player() # print('Response dictionary: \n\r{}'.format(json.dumps(response_dict, indent=2))) currency_1 = "0" currency_2 = "0" @@ -368,15 +373,11 @@ def _print_character_info(self): logger.log('') def use_lucky_egg(self): - self.api.use_item_xp_boost(item_id=301) - inventory_req = self.api.call() - return inventory_req + return self.api.use_item_xp_boost(item_id=301) def get_inventory(self): if self.latest_inventory is None: - self.api.get_inventory() - response = self.api.call() - self.latest_inventory = response + self.latest_inventory = self.api.get_inventory() return self.latest_inventory def update_inventory(self): @@ -538,9 +539,10 @@ def heartbeat(self): self.fort_timeouts = {id: timeout for id, timeout in self.fort_timeouts.iteritems() if timeout >= time.time() * 1000} - self.api.get_player() - self.api.check_awarded_badges() - self.api.call() + request = self.api.create_request() + request.get_player() + request.check_awarded_badges() + request.call() self.update_web_location() # updates every tick def get_inventory_count(self, what): @@ -620,14 +622,12 @@ def get_map_objects(self, lat, lng, timestamp, cellid): if time.time() - self.last_time_map_object < self.config.map_object_cache_time: return self.last_map_object - self.api.get_map_objects( + self.last_map_object = self.api.get_map_objects( latitude=f2i(lat), longitude=f2i(lng), since_timestamp_ms=timestamp, cell_id=cellid ) - - self.last_map_object = self.api.call() self.last_time_map_object = time.time() return self.last_map_object diff --git a/pokemongo_bot/api_wrapper.py b/pokemongo_bot/api_wrapper.py index f861cb1636..0cb3f9c62f 100644 --- a/pokemongo_bot/api_wrapper.py +++ b/pokemongo_bot/api_wrapper.py @@ -1,40 +1,70 @@ import time -from pgoapi.exceptions import (NotLoggedInException, - ServerBusyOrOfflineException) +from pgoapi.exceptions import ServerSideRequestThrottlingException, NotLoggedInException, ServerBusyOrOfflineException, NoPlayerPositionSetException, EmptySubrequestChainException +from pgoapi.pgoapi import PGoApi, PGoApiRequest, RpcApi +from pgoapi.protos.POGOProtos.Networking.Requests_pb2 import RequestType -import logger +import pokemongo_bot.logger as logger from human_behaviour import sleep - -class ApiWrapper(object): - def __init__(self, api): - self._api = api +class ApiWrapper(PGoApi): + def __init__(self): + PGoApi.__init__(self) + self.useVanillaRequest = False + + def create_request(self): + RequestClass = ApiRequest + if self.useVanillaRequest: + RequestClass = PGoApiRequest + + return RequestClass( + self._api_endpoint, + self._auth_provider, + self._position_lat, + self._position_lng, + self._position_alt + ) + + def login(self, *args): + # login needs base class "create_request" + self.useVanillaRequest = True + try: + ret_value = PGoApi.login(self, *args) + finally: + # cleanup code + self.useVanillaRequest = False + return ret_value + + +class ApiRequest(PGoApiRequest): + def __init__(self, *args): + PGoApiRequest.__init__(self, *args) self.request_callers = [] - self.reset_auth() self.last_api_request_time = None self.requests_per_seconds = 2 - def reset_auth(self): - self._api._auth_token = None - self._api._auth_provider = None - self._api._api_endpoint = None - - # wrap api methods - def _can_call(self): - if not self._api._req_method_list or len(self._api._req_method_list) == 0: - raise RuntimeError('Trying to call the api without setting any request') - if self._api._auth_provider is None or not self._api._auth_provider.is_login(): - logger.log('Not logged in!', 'red') + def can_call(self): + if not self._req_method_list: + raise EmptySubrequestChainException() + + if (self._position_lat is None) or (self._position_lng is None) or (self._position_alt is None): + raise NoPlayerPositionSetException() + + if self._auth_provider is None or not self._auth_provider.is_login(): + self.log.info('Not logged in') raise NotLoggedInException() + return True + def _call(self): + return PGoApiRequest.call(self) + def _pop_request_callers(self): r = self.request_callers self.request_callers = [] return [i.upper() for i in r] - def _is_response_valid(self, result, request_callers): + def is_response_valid(self, result, request_callers): if not result or result is None or not isinstance(result, dict): return False @@ -54,22 +84,35 @@ def _is_response_valid(self, result, request_callers): def call(self, max_retry=5): request_callers = self._pop_request_callers() - if not self._can_call(): + if not self.can_call(): return False # currently this is never ran, exceptions are raised before request_timestamp = None - - api_req_method_list = self._api._req_method_list + api_req_method_list = self._req_method_list result = None try_cnt = 0 + throttling_retry = 0 while True: request_timestamp = self.throttle_sleep() - self._api._req_method_list = [req_method for req_method in api_req_method_list] # api internally clear this field after a call - result = self._api.call() - if not self._is_response_valid(result, request_callers): + # self._call internally clear this field, so save it + self._req_method_list = [req_method for req_method in api_req_method_list] + try: + result = self._call() + should_retry = False + except ServerSideRequestThrottlingException: + should_retry = True + + if should_retry: + throttling_retry += 1 + logger.log("Server is throttling, let's slow down a bit") + if throttling_retry >= max_retry: + raise ServerSideRequestThrottlingException('Server throttled too many times') + sleep(1) # huge sleep ? + continue # skip response checking + + if not self.is_response_valid(result, request_callers): try_cnt += 1 logger.log('Server seems to be busy or offline - try again - {}/{}'.format(try_cnt, max_retry), 'red') - if try_cnt >= max_retry: raise ServerBusyOrOfflineException() sleep(1) @@ -79,15 +122,10 @@ def call(self, max_retry=5): self.last_api_request_time = request_timestamp return result - def login(self, provider, username, password): - return self._api.login(provider, username, password) - - # fallback def __getattr__(self, func): - DEFAULT_ATTRS = ['_position_lat', '_position_lng', '_auth_provider', '_api_endpoint', 'set_position', 'get_position'] - if func not in DEFAULT_ATTRS: + if func.upper() in RequestType.keys(): self.request_callers.append(func) - return getattr(self._api, func) + return PGoApiRequest.__getattr__(self, func) def throttle_sleep(self): now_milliseconds = time.time() * 1000 @@ -95,7 +133,7 @@ def throttle_sleep(self): difference = now_milliseconds - (self.last_api_request_time if self.last_api_request_time else 0) - if (self.last_api_request_time != None and difference < required_delay_between_requests): + if self.last_api_request_time != None and difference < required_delay_between_requests: sleep_time = required_delay_between_requests - difference time.sleep(sleep_time / 1000) diff --git a/pokemongo_bot/cell_workers/collect_level_up_reward.py b/pokemongo_bot/cell_workers/collect_level_up_reward.py index 8398153386..912a97d197 100644 --- a/pokemongo_bot/cell_workers/collect_level_up_reward.py +++ b/pokemongo_bot/cell_workers/collect_level_up_reward.py @@ -25,8 +25,7 @@ def work(self): self.previous_level = self.current_level def _collect_level_reward(self): - self.bot.api.level_up_rewards(level=self.current_level) - response_dict = self.bot.api.call() + response_dict = self.bot.api.level_up_rewards(level=self.current_level) if 'status_code' in response_dict and response_dict['status_code'] == 1: data = (response_dict .get('responses', {}) diff --git a/pokemongo_bot/cell_workers/evolve_all.py b/pokemongo_bot/cell_workers/evolve_all.py index 7a7a19e020..cdc6389344 100644 --- a/pokemongo_bot/cell_workers/evolve_all.py +++ b/pokemongo_bot/cell_workers/evolve_all.py @@ -9,9 +9,10 @@ def initialize(self): self.evolve_speed = self.config.get('evolve_speed', 3.7) self.evolve_cp_min = self.config.get('evolve_cp_min', 300) self.use_lucky_egg = self.config.get('use_lucky_egg', False) + self._validate_config() def _validate_config(self): - if isinstance(self.evolve_all, str): + if isinstance(self.evolve_all, basestring): self.evolve_all = [str(pokemon_name) for pokemon_name in self.evolve_all.split(',')] def work(self): @@ -156,8 +157,7 @@ def _execute_pokemon_evolve(self, pokemon, cache): if pokemon_name in cache: return - self.bot.api.evolve_pokemon(pokemon_id=pokemon_id) - response_dict = self.bot.api.call() + response_dict = self.bot.api.evolve_pokemon(pokemon_id=pokemon_id) status = response_dict['responses']['EVOLVE_POKEMON']['result'] if status == 1: logger.log('[#] Successfully evolved {} with {} CP and {} IV!'.format( @@ -173,8 +173,7 @@ def _execute_pokemon_evolve(self, pokemon, cache): # TODO: move to utils. These methods are shared with other workers. def transfer_pokemon(self, pid): - self.bot.api.release_pokemon(pokemon_id=pid) - response_dict = self.bot.api.call() + response_dict = self.bot.api.release_pokemon(pokemon_id=pid) def count_pokemon_inventory(self): response_dict = self.bot.get_inventory() diff --git a/pokemongo_bot/cell_workers/follow_spiral.py b/pokemongo_bot/cell_workers/follow_spiral.py index a77363403d..bda348490a 100644 --- a/pokemongo_bot/cell_workers/follow_spiral.py +++ b/pokemongo_bot/cell_workers/follow_spiral.py @@ -1,24 +1,30 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals + +import math + import pokemongo_bot.logger as logger from pokemongo_bot.cell_workers.utils import distance, format_dist from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.cell_workers.base_task import BaseTask - class FollowSpiral(BaseTask): def initialize(self): - self.steplimit = self.bot.config.max_steps + self.steplimit = self.config.get("diameter", 4) + self.step_size = self.config.get("step_size", 70) self.origin_lat = self.bot.position[0] self.origin_lon = self.bot.position[1] + self.diameter_to_steps = (self.steplimit+1) ** 2 self.points = self._generate_spiral( - self.origin_lat, self.origin_lon, 0.0018, self.steplimit + self.origin_lat, self.origin_lon, self.step_size, self.diameter_to_steps ) + self.ptr = 0 self.direction = 1 self.cnt = 0 + @staticmethod def _generate_spiral(starting_lat, starting_lng, step_size, step_limit): """ @@ -34,18 +40,24 @@ def _generate_spiral(starting_lat, starting_lng, step_size, step_limit): coords = [{'lat': starting_lat, 'lng': starting_lng}] steps, x, y, d, m = 1, 0, 0, 1, 1 + rlat = starting_lat * math.pi + latdeg = 111132.93 - 559.82 * math.cos(2*rlat) + 1.175*math.cos(4*rlat) + lngdeg = 111412.84 * math.cos(rlat) - 93.5 * math.cos(3*rlat) + step_size_lat = step_size / latdeg + step_size_lng = step_size / lngdeg + while steps < step_limit: while 2 * x * d < m and steps < step_limit: x = x + d steps += 1 - lat = x * step_size + starting_lat - lng = y * step_size + starting_lng + lat = x * step_size_lat + starting_lat + lng = y * step_size_lng + starting_lng coords.append({'lat': lat, 'lng': lng}) while 2 * y * d < m and steps < step_limit: y = y + d steps += 1 - lat = x * step_size + starting_lat - lng = y * step_size + starting_lng + lat = x * step_size_lat + starting_lat + lng = y * step_size_lng + starting_lng coords.append({'lat': lat, 'lng': lng}) d *= -1 @@ -88,9 +100,12 @@ def work(self): point['lat'], point['lng'] ) <= 1 or (self.bot.config.walk > 0 and step_walker == None): - if self.ptr + self.direction == len(self.points) or self.ptr + self.direction == -1: + if self.ptr + self.direction >= len(self.points) or self.ptr + self.direction <= -1: self.direction *= -1 - self.ptr += self.direction + if len(self.points) != 1: + self.ptr += self.direction + else: + self.ptr = 0 self.cnt = 0 return [point['lat'], point['lng']] diff --git a/pokemongo_bot/cell_workers/handle_soft_ban.py b/pokemongo_bot/cell_workers/handle_soft_ban.py index d13bd63eea..04e5178b50 100644 --- a/pokemongo_bot/cell_workers/handle_soft_ban.py +++ b/pokemongo_bot/cell_workers/handle_soft_ban.py @@ -50,7 +50,6 @@ def spin_fort(self, fort): player_latitude=f2i(self.bot.position[0]), player_longitude=f2i(self.bot.position[1]) ) - self.bot.api.call() def should_run(self): return self.bot.softban diff --git a/pokemongo_bot/cell_workers/incubate_eggs.py b/pokemongo_bot/cell_workers/incubate_eggs.py index b8ef560bbb..95d497518a 100644 --- a/pokemongo_bot/cell_workers/incubate_eggs.py +++ b/pokemongo_bot/cell_workers/incubate_eggs.py @@ -47,8 +47,10 @@ def _apply_incubators(self): continue if self.bot.config.debug: logger.log('[x] Attempting to apply incubator {} to egg {}'.format(incubator['id'], egg['id'])) - self.bot.api.use_item_egg_incubator(item_id=incubator["id"], pokemon_id=egg["id"]) - ret = self.bot.api.call() + ret = self.bot.api.use_item_egg_incubator( + item_id=incubator["id"], + pokemon_id=egg["id"] + ) if ret: code = ret.get("responses", {}).get("USE_ITEM_EGG_INCUBATOR", {}).get("result", 0) if code == 1: @@ -125,8 +127,7 @@ def _check_inventory(self, lookup_ids=[]): return matched_pokemon def _hatch_eggs(self): - self.bot.api.get_hatched_eggs() - response_dict = self.bot.api.call() + response_dict = self.bot.api.get_hatched_eggs() log_color = 'green' try: result = reduce(dict.__getitem__, ["responses", "GET_HATCHED_EGGS"], response_dict) diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index d5797be603..2313cc504d 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -10,8 +10,8 @@ class MoveToFort(BaseTask): def initialize(self): self.lure_distance = 0 - self.lure_attraction = self.config.get("lure_attraction", True) - self.lure_max_distance = self.config.get("lure_max_distance", 2000) + self.lure_attraction = True #self.config.get("lure_attraction", True) + self.lure_max_distance = 2000 #self.config.get("lure_max_distance", 2000) def should_run(self): has_space_for_loot = self.bot.has_space_for_loot() diff --git a/pokemongo_bot/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py index d626595d11..2c61d87a9f 100644 --- a/pokemongo_bot/cell_workers/nickname_pokemon.py +++ b/pokemongo_bot/cell_workers/nickname_pokemon.py @@ -7,7 +7,7 @@ def initialize(self): self.template = self.config.get('nickname_template','').lower().strip() if self.template == "{name}": self.template = "" - + def work(self): try: inventory = reduce(dict.__getitem__, ["responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], self.bot.get_inventory()) @@ -16,8 +16,8 @@ def work(self): else: pokemon_data = self._get_inventory_pokemon(inventory) for pokemon in pokemon_data: - self._nickname_pokemon(pokemon) - + self._nickname_pokemon(pokemon) + def _get_inventory_pokemon(self,inventory_dict): pokemon_data = [] for inv_data in inventory_dict: @@ -29,7 +29,7 @@ def _get_inventory_pokemon(self,inventory_dict): if not pokemon.get('is_egg',False): pokemon_data.append(pokemon) return pokemon_data - + def _nickname_pokemon(self,pokemon): """This requies a pokemon object containing all the standard fields: id, ivs, cp, etc""" new_name = "" @@ -62,10 +62,9 @@ def _nickname_pokemon(self,pokemon): logger.log("Unable to nickname {} due to bad template ({})".format(name,bad_key),log_color) if pokemon.get('nickname', "") == new_name: return - self.bot.api.nickname_pokemon(pokemon_id=instance_id,nickname=new_name) - response = self.bot.api.call() + response = self.bot.api.nickname_pokemon(pokemon_id=instance_id,nickname=new_name) sleep(1.2) - try: + try: result = reduce(dict.__getitem__, ["responses", "NICKNAME_POKEMON"], response) except KeyError: logger.log("Attempt to nickname received bad response from server.",log_color) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 6e90472a0c..87daf99d50 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -4,6 +4,8 @@ from pokemongo_bot import logger from pokemongo_bot.human_behaviour import (normalized_reticle_size, sleep, spin_modifier) +from random import random + class PokemonCatchWorker(object): BAG_FULL = 'bag_full' @@ -93,8 +95,11 @@ def work(self): break # Use the berry to catch - self.api.use_item_capture(item_id = berry_id,encounter_id = encounter_id,spawn_point_id = self.spawn_point_guid) - response_dict = self.api.call() + response_dict = self.api.use_item_capture( + item_id=berry_id, + encounter_id=encounter_id, + spawn_point_id=self.spawn_point_guid + ) if response_dict and response_dict['status_code'] is 1 and 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: for i in range(len(catch_rate)): if 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: @@ -126,8 +131,10 @@ def work(self): success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) logger.log('Catch Rate with normal Pokeball is low ({}%). Thinking to throw a {}... ({} left!)'.format(success_percentage,self.item_list[str(berry_id)],berries_count-1)) - self.api.use_item_capture(item_id = berry_id,encounter_id = encounter_id,spawn_point_id = self.spawn_point_guid) - response_dict = self.api.call() + response_dict = self.api.use_item_capture(item_id=berry_id, + encounter_id=encounter_id, + spawn_point_id=self.spawn_point_guid + ) if response_dict and response_dict['status_code'] is 1 and 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: for i in range(len(catch_rate)): if 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: @@ -159,8 +166,10 @@ def work(self): success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) logger.log('Catch Rate with normal Pokeball is low ({}%). Thinking to throw a {}... ({} left!)'.format(success_percentage,self.item_list[str(berry_id)],berries_count-1)) - self.api.use_item_capture(item_id = berry_id,encounter_id = encounter_id,spawn_point_id = self.spawn_point_guid) - response_dict = self.api.call() + response_dict = self.api.use_item_capture(item_id=berry_id, + encounter_id=encounter_id, + spawn_point_id=self.spawn_point_guid + ) if response_dict and response_dict['status_code'] is 1 and 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: for i in range(len(catch_rate)): if 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: @@ -192,27 +201,34 @@ def work(self): if catch_rate[pokeball-1]<0.30 and items_stock[3]>0: pokeball = 3 + # Randomize the quality of the throw + # Example of the structure + throw_parameters = {'normalized_reticle_size': 1.950, + 'spin_modifier': 1.0, + 'normalized_hit_position': 1.0, + 'throw_type_label' : 'Excellent'} + self.generate_random_throw_parameters_if_necessary(throw_parameters) + items_stock[pokeball] -= 1 success_percentage = '{0:.2f}'.format(catch_rate[pokeball - 1] * 100) - logger.log('Using {} (chance: {}%)... ({} left!)'.format( + logger.log('Using {} ({}), (chance: {}%)... ({} left!)'.format( self.item_list[str(pokeball)], + throw_parameters.get('throw_type_label'), success_percentage, items_stock[pokeball] )) id_list1 = self.count_pokemon_inventory() - reticle_size_parameter = normalized_reticle_size(self.config.catch_randomize_reticle_factor) - spin_modifier_parameter = spin_modifier(self.config.catch_randomize_spin_factor) - - self.api.catch_pokemon(encounter_id=encounter_id, - pokeball=pokeball, - normalized_reticle_size=reticle_size_parameter, - spawn_point_id=self.spawn_point_guid, - hit_pokemon=1, - spin_modifier=spin_modifier_parameter, - normalized_hit_position=1) - response_dict = self.api.call() + response_dict = self.api.catch_pokemon( + encounter_id=encounter_id, + pokeball=pokeball, + normalized_reticle_size=throw_parameters['normalized_reticle_size'], + spawn_point_id=self.spawn_point_guid, + hit_pokemon=1, + spin_modifier=throw_parameters['spin_modifier'], + normalized_hit_position=throw_parameters['normalized_hit_position'] + ) if response_dict and \ 'responses' in response_dict and \ @@ -254,8 +270,7 @@ def work(self): if len(pokemon_to_transfer) == 0: raise RuntimeError( 'Trying to evolve 0 pokemons!') - self.api.evolve_pokemon(pokemon_id=pokemon_to_transfer[0]) - response_dict = self.api.call() + response_dict = self.api.evolve_pokemon(pokemon_id=pokemon_to_transfer[0]) status = response_dict['responses']['EVOLVE_POKEMON']['result'] if status == 1: logger.log( @@ -269,8 +284,7 @@ def work(self): def count_pokemon_inventory(self): # don't use cached bot.get_inventory() here # because we need to have actual information in capture logic - self.api.get_inventory() - response_dict = self.api.call() + response_dict = self.api.get_inventory() id_list = [] callback = lambda pokemon: id_list.append(pokemon['id']) @@ -360,22 +374,30 @@ def create_encounter_api_call(self): player_latitude = self.pokemon['latitude'] player_longitude = self.pokemon['longitude'] + request = self.api.create_request() if 'spawn_point_id' in self.pokemon: spawn_point_id = self.pokemon['spawn_point_id'] self.spawn_point_guid = spawn_point_id self.response_key = 'ENCOUNTER' self.response_status_key = 'status' - self.api.encounter(encounter_id=encounter_id, spawn_point_id=spawn_point_id, - player_latitude=player_latitude, player_longitude=player_longitude) + request.encounter( + encounter_id=encounter_id, + spawn_point_id=spawn_point_id, + player_latitude=player_latitude, + player_longitude=player_longitude + ) else: fort_id = self.pokemon['fort_id'] self.spawn_point_guid = fort_id self.response_key = 'DISK_ENCOUNTER' self.response_status_key = 'result' - self.api.disk_encounter(encounter_id=encounter_id, fort_id=fort_id, - player_latitude=player_latitude, player_longitude=player_longitude) - - return self.api.call() + request.disk_encounter( + encounter_id=encounter_id, + fort_id=fort_id, + player_latitude=player_latitude, + player_longitude=player_longitude + ) + return request.call() def check_vip_pokemon(self,pokemon, cp, iv): @@ -404,3 +426,51 @@ def check_vip_pokemon(self,pokemon, cp, iv): } return logic_to_function[cp_iv_logic](*catch_results.values()) + def generate_random_throw_parameters_if_necessary(self, throw_parameters): + + # First, randomize spin + spin_success_rate = self.config.catch_throw_parameters.get('spin_success_rate', 1) + + if random() <= spin_success_rate: + throw_parameters['spin_modifier'] = 0.5 + 0.5 * random() + else: + throw_parameters['spin_modifier'] = 0.499 * random() + + # Then, randomize the throw quality + throw_excellent_chance = self.config.catch_throw_parameters.get('excellent_rate', 0) + throw_great_chance = self.config.catch_throw_parameters.get('great_rate', 0) + throw_nice_chance = self.config.catch_throw_parameters.get('nice_rate', 0) + throw_normal_throw_chance = self.config.catch_throw_parameters.get('normal_rate', 0) + + # Total every chance types, pick a random number in the range and check what type of throw we got + total_chances = throw_excellent_chance + throw_great_chance \ + + throw_nice_chance + throw_normal_throw_chance + + random_throw = random() * total_chances + + if random_throw <= throw_excellent_chance: + throw_parameters['normalized_reticle_size'] = 1.70 + 0.25*random() + throw_parameters['normalized_hit_position'] = 1.0 + throw_parameters['throw_type_label'] = 'Excellent' + return + + random_throw -= throw_excellent_chance + if random_throw <= throw_great_chance: + throw_parameters['normalized_reticle_size'] = 1.30 + 0.399 * random() + throw_parameters['normalized_hit_position'] = 1.0 + throw_parameters['throw_type_label'] = 'Great' + return + + random_throw -= throw_great_chance + if random_throw <= throw_nice_chance: + throw_parameters['normalized_reticle_size'] = 1.00 + 0.299 * random() + throw_parameters['normalized_hit_position'] = 1.0 + throw_parameters['throw_type_label'] = 'Nice' + return + + # Not a any kind of special throw, let's throw a normal one + # Here the reticle size doesn't matter, we scored out of it + throw_parameters['normalized_reticle_size'] = 1.25 + 0.70 * random() + throw_parameters['normalized_hit_position'] = 0.0 + throw_parameters['throw_type_label'] = 'Normal' + diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 2ece62f95b..022a711f1a 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -44,9 +44,9 @@ def work(self): logger.log("-- Failed to discard " + item_name, 'red') def send_recycle_item_request(self, item_id, count): - self.bot.api.recycle_inventory_item(item_id=item_id, count=count) - inventory_req = self.bot.api.call() - # 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 inventory_req + return self.bot.api.recycle_inventory_item( + item_id=item_id, + count=count + ) diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 6534731a86..f34fc2b8ef 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -33,12 +33,13 @@ def work(self): logger.log('Now at Pokestop: {0}'.format(fort_name), 'cyan') logger.log('Spinning ...', 'cyan') - self.bot.api.fort_search(fort_id=fort['id'], - fort_latitude=lat, - fort_longitude=lng, - player_latitude=f2i(self.bot.position[0]), - player_longitude=f2i(self.bot.position[1])) - response_dict = self.bot.api.call() + response_dict = self.bot.api.fort_search( + fort_id=fort['id'], + fort_latitude=lat, + fort_longitude=lng, + player_latitude=f2i(self.bot.position[0]), + player_longitude=f2i(self.bot.position[1]) + ) if 'responses' in response_dict and \ 'FORT_SEARCH' in response_dict['responses']: diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 3cf40413f5..374ffa6a87 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -71,8 +71,10 @@ def work(self): def _release_pokemon_get_groups(self): pokemon_groups = {} - self.bot.api.get_player().get_inventory() - inventory_req = self.bot.api.call() + request = self.bot.api.create_request() + request.get_player() + request.get_inventory() + inventory_req = request.call() if inventory_req.get('responses', False) is False: return pokemon_groups @@ -182,8 +184,7 @@ def release_pokemon(self, pokemon_name, cp, iv, pokemon_id): logger.log('Exchanging {} [CP {}] [Potential {}] for candy!'.format(pokemon_name, cp, iv), 'green') - self.bot.api.release_pokemon(pokemon_id=pokemon_id) - response_dict = self.bot.api.call() + response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon_id) action_delay(self.bot.config.action_wait_min, self.bot.config.action_wait_max) def _get_release_config_for(self, pokemon): diff --git a/pokemongo_bot/cell_workers/utils.py b/pokemongo_bot/cell_workers/utils.py index 253083bc0b..946c757b16 100644 --- a/pokemongo_bot/cell_workers/utils.py +++ b/pokemongo_bot/cell_workers/utils.py @@ -28,10 +28,10 @@ def fort_details(bot, fort_id, latitude, longitude): """ Lookup the fort details and cache the response for future use. """ - bot.api.fort_details(fort_id=fort_id, latitude=latitude, longitude=longitude) - + request = bot.api.create_request() + request.fort_details(fort_id=fort_id, latitude=latitude, longitude=longitude) try: - response_dict = bot.api.call() + response_dict = request.call() FORT_CACHE[fort_id] = response_dict['responses']['FORT_DETAILS'] except Exception: pass diff --git a/pokemongo_bot/human_behaviour.py b/pokemongo_bot/human_behaviour.py index 2a8d2d5e9f..e18cdb22a1 100644 --- a/pokemongo_bot/human_behaviour.py +++ b/pokemongo_bot/human_behaviour.py @@ -24,23 +24,3 @@ def random_lat_long_delta(): # Return random value from [-.000025, .000025]. Since 364,000 feet is equivalent to one degree of latitude, this # should be 364,000 * .000025 = 9.1. So it returns between [-9.1, 9.1] return ((random() * 0.00001) - 0.000005) * 5 - - -# Humanized `normalized_reticle_size` parameter for `catch_pokemon` API. -# 1.0 => normal, 1.950 => excellent -def normalized_reticle_size(factor): - minimum = 1.0 - maximum = 1.950 - return uniform( - minimum + (maximum - minimum) * factor, - maximum) - - -# Humanized `spin_modifier` parameter for `catch_pokemon` API. -# 0.0 => normal ball, 1.0 => super spin curve ball -def spin_modifier(factor): - minimum = 0.0 - maximum = 1.0 - return uniform( - minimum + (maximum - minimum) * factor, - maximum) diff --git a/pokemongo_bot/metrics.py b/pokemongo_bot/metrics.py index 0ab7cb14dd..0ffeb39a6c 100644 --- a/pokemongo_bot/metrics.py +++ b/pokemongo_bot/metrics.py @@ -70,9 +70,10 @@ def released_pokemon(self, count=1): self.releases += count def capture_stats(self): - self.bot.api.get_inventory() - self.bot.api.get_player() - response_dict = self.bot.api.call() + request = self.bot.api.create_request() + request.get_inventory() + request.get_player() + response_dict = request.call() try: self.dust['latest'] = response_dict['responses']['GET_PLAYER']['player_data']['currencies'][1]['amount'] if self.dust['start'] is None: self.dust['start'] = self.dust['latest'] diff --git a/requirements.txt b/requirements.txt index e572c4e2c6..08f9407c81 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy==1.11.0 networkx==1.11 --e git+https://github.com/tejado/pgoapi.git@81e786cabf027a1c8fbd1e9a07e1c11aa3d8ee8b#egg=pgoapi +-e git+https://github.com/tejado/pgoapi.git@0811db23d639039f968a82e06c7aa15a0a5016b6#egg=pgoapi geopy==1.11.0 protobuf==3.0.0b4 requests==2.10.0 diff --git a/tests/__init__.py b/tests/__init__.py index 2db23ffd9d..5318e2d482 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,21 +1,15 @@ # __init__.py from mock import MagicMock -from pgoapi import PGoApi -from pokemongo_bot.api_wrapper import ApiWrapper +from pokemongo_bot.api_wrapper import ApiWrapper, ApiRequest from pokemongo_bot import PokemonGoBot class FakeApi(ApiWrapper): - def __init__(self, return_value=None): - super(FakeApi, self).__init__(PGoApi()) - self._api.call = MagicMock(return_value=return_value) - - def _can_call(self): - return True - - def setApiReturnValue(self, value): - self._api.call.return_value = value - + def create_request(self, return_value='mock return'): + request = ApiWrapper.create_request(self) + request.can_call = MagicMock(return_value=True) + request._call = MagicMock(return_value=return_value) + return request class FakeBot(PokemonGoBot): def __init__(self): diff --git a/tests/api_wrapper_test.py b/tests/api_wrapper_test.py index 0a835e0e20..335f04cbf2 100644 --- a/tests/api_wrapper_test.py +++ b/tests/api_wrapper_test.py @@ -5,43 +5,56 @@ from tests import FakeApi from pgoapi import PGoApi -from pgoapi.exceptions import NotLoggedInException, ServerBusyOrOfflineException +from pgoapi.exceptions import NotLoggedInException, ServerBusyOrOfflineException, NoPlayerPositionSetException, EmptySubrequestChainException from pokemongo_bot.api_wrapper import ApiWrapper class TestApiWrapper(unittest.TestCase): def test_raises_not_logged_in_exception(self): - api = ApiWrapper(PGoApi()) - api.get_inventory(test='awesome') + api = ApiWrapper() + api.set_position(*(42, 42, 0)) + request = api.create_request() + request.get_inventory(test='awesome') with self.assertRaises(NotLoggedInException): - api.call() + request.call() def test_api_call_with_no_requests_set(self): - api = ApiWrapper(PGoApi()) - with self.assertRaises(RuntimeError): - api.call() + request = ApiWrapper().create_request() + with self.assertRaises(EmptySubrequestChainException): + request.call() + + def test_api_wrong_request(self): + request = ApiWrapper().create_request() + with self.assertRaises(AttributeError): + request.wrong_request() + + def test_raises_no_player_position_set_exception(self): + request = ApiWrapper().create_request() + request.get_inventory(test='awesome') + with self.assertRaises(NoPlayerPositionSetException): + request.call() @patch('pokemongo_bot.api_wrapper.sleep') def test_api_server_is_unreachable_raises_server_busy_or_offline_exception(self, sleep): sleep.return_value = True # we don't need to really sleep - api = FakeApi('Wrong Value') - api.get_inventory(test='awesome') + request = FakeApi().create_request('Wrong Value') + request.get_inventory() # we expect an exception because the "server" isn't returning a valid response with self.assertRaises(ServerBusyOrOfflineException): - api.call() + request.call() def test_mocked_call(self): - api = FakeApi(True) - api._is_response_valid = MagicMock(return_value=True) - api.get_inventory(test='awesome') - result = api.call() + request = FakeApi().create_request(True) + request.is_response_valid = MagicMock(return_value=True) + request.get_inventory(test='awesome') + result = request.call() self.assertTrue(result) def test_return_value_is_not_valid(self): - - def returnApi(ret_value): - api = FakeApi(ret_value) - api.get_inventory(test='awesome') - return api + api = FakeApi() + def returnRequest(ret_value): + request = api.create_request(ret_value) + request.get_inventory(test='awesome') + return request wrong_return_values = [ None, @@ -52,52 +65,59 @@ def returnApi(ret_value): {'responses': {'GET_INVENTORY_OR_NOT': {}}, 'status_code': 0} ] for wrong in wrong_return_values: - api = returnApi(wrong) - request_callers = api._pop_request_callers() # we can pop because we do no call + request = returnRequest(wrong) + request_callers = request._pop_request_callers() # we can pop because we do no call - is_valid = api._is_response_valid(wrong, request_callers) + is_valid = request.is_response_valid(wrong, request_callers) self.assertFalse(is_valid, 'return value {} is valid somehow ?'.format(wrong)) def test_return_value_is_valid(self): - api = FakeApi() # we set the return value below - api.get_inventory(test='awesome') + request = FakeApi().create_request() # we set the return value below + request.get_inventory(test='awesome') - request = api.request_callers[0] # only one request - self.assertEqual(request.upper(), 'GET_INVENTORY') + request_caller = request.request_callers[0] # only one request + self.assertEqual(request_caller.upper(), 'GET_INVENTORY') - good_return_value = {'responses': {request.upper(): {}}, 'status_code': 0} - api.setApiReturnValue(good_return_value) + good_return_value = {'responses': {request_caller.upper(): {}}, 'status_code': 0} + request._call.return_value = good_return_value - result = api.call() + result = request.call() self.assertEqual(result, good_return_value) - self.assertEqual(len(api.request_callers), 0, 'request_callers must be empty') + self.assertEqual(len(request.request_callers), 0, 'request_callers must be empty') def test_multiple_requests(self): - api = FakeApi() - api.get_inventory(test='awesome') - api.fort_details() + request = FakeApi().create_request() + request.get_inventory(test='awesome') + request.fort_details() good_return_value = {'responses': {'GET_INVENTORY': {}, 'FORT_DETAILS': {}}, 'status_code': 0} - api.setApiReturnValue(good_return_value) + request._call.return_value = good_return_value - result = api.call() + result = request.call() self.assertEqual(result, good_return_value) @timeout(1) def test_api_call_throttle_should_pass(self): - api = FakeApi(True) - api._is_response_valid = MagicMock(return_value=True) - api.requests_per_seconds = 5 + request = FakeApi().create_request() + request.is_response_valid = MagicMock(return_value=True) + request.requests_per_seconds = 5 - for i in range(api.requests_per_seconds): - api.call() + for i in range(request.requests_per_seconds): + request.call() @timeout(1) # expects a timeout def test_api_call_throttle_should_fail(self): - api = FakeApi(True) - api._is_response_valid = MagicMock(return_value=True) - api.requests_per_seconds = 5 + request = FakeApi().create_request() + request.is_response_valid = MagicMock(return_value=True) + request.requests_per_seconds = 5 with self.assertRaises(TimeoutError): - for i in range(api.requests_per_seconds * 2): - api.call() + for i in range(request.requests_per_seconds * 2): + request.call() + + @patch('pokemongo_bot.api_wrapper.ApiRequest.is_response_valid') + def test_api_direct_call(self, mock_method): + mock_method.return_value = True + + result = FakeApi().get_inventory() + self.assertEqual(result, 'mock return') diff --git a/web b/web index ca7c9e134b..6ba5609c61 160000 --- a/web +++ b/web @@ -1 +1 @@ -Subproject commit ca7c9e134bcfd1c7047f873fa1071d026491a0c2 +Subproject commit 6ba5609c6151507b5b832a74e471b6b7b1a182c9