Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some XP efficiency improvements #2889

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 10 additions & 2 deletions pokemongo_bot/__init__.py
Expand Up @@ -8,6 +8,7 @@
import random
import re
import sys
import struct
import time

from geopy.geocoders import GoogleV3
Expand All @@ -29,7 +30,9 @@
from worker_result import WorkerResult
from tree_config_builder import ConfigException, MismatchTaskApiVersion, TreeConfigBuilder
from sys import platform as _platform
import struct



class PokemonGoBot(object):
@property
def position(self):
Expand Down Expand Up @@ -391,11 +394,16 @@ def tick(self):

# Check if session token has expired
self.check_session(self.position[0:2])
start_tick = time.time()

for worker in self.workers:
if worker.work() == WorkerResult.RUNNING:
return

end_tick = time.time()
if end_tick - start_tick < 5:
time.sleep(5 - (end_tick - start_tick))

def get_meta_cell(self):
location = self.position[0:2]
cells = self.find_close_cells(*location)
Expand Down Expand Up @@ -540,7 +548,7 @@ def check_session(self, position):

# prevent crash if return not numeric value
if not self.is_numeric(self.api._auth_provider._ticket_expire):
self.logger.info("Ticket expired value is not numeric", 'yellow')
self.logger.info("Ticket expired value is not numeric")
return

remaining_time = \
Expand Down
66 changes: 39 additions & 27 deletions pokemongo_bot/base_task.py
@@ -1,31 +1,43 @@
import logging
import time


class BaseTask(object):
TASK_API_VERSION = 1

def __init__(self, bot, config):
self.bot = bot
self.config = config
self._validate_work_exists()
self.logger = logging.getLogger(type(self).__name__)
self.initialize()

def _validate_work_exists(self):
method = getattr(self, 'work', None)
if not method or not callable(method):
raise NotImplementedError('Missing "work" method')

def emit_event(self, event, sender=None, level='info', formatted='', data={}):
if not sender:
sender=self
self.bot.event_manager.emit(
event,
sender=sender,
level=level,
formatted=formatted,
data=data
)

def initialize(self):
pass
TASK_API_VERSION = 1

def __init__(self, bot, config):
self.bot = bot
self.config = config
self._validate_work_exists()
self.logger = logging.getLogger(type(self).__name__)
self.last_ran = time.time()
self.run_interval = config.get('run_interval', 10)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this should default to None. It will always run every tick unless both the task utilizes _time_to_run, and the user configures how frequently..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TheSavior the default value is 10 to make it work if the task does run in a time-based way. Otherwise if task is time-based and user don't provide run_interval every time he will see an error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless the task was written such that it would delay only if it was configured with an interval. Otherwise it would run every tick

self.initialize()

def _update_last_ran(self):
self.last_ran = time.time()

def _time_to_run(self):
interval = time.time() - self.last_ran
if interval > self.run_interval:
return True
return False

def _validate_work_exists(self):
method = getattr(self, 'work', None)
if not method or not callable(method):
raise NotImplementedError('Missing "work" method')

def emit_event(self, event, sender=None, level='info', formatted='', data={}):
if not sender:
sender=self
self.bot.event_manager.emit(
event,
sender=sender,
level=level,
formatted=formatted,
data=data
)

def initialize(self):
pass
40 changes: 27 additions & 13 deletions pokemongo_bot/cell_workers/catch_lured_pokemon.py
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from pokemongo_bot.cell_workers.utils import fort_details
from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker
from pokemongo_bot.base_task import BaseTask
from pokemongo_bot.constants import Constants
from pokemongo_bot.cell_workers.utils import fort_details, distance
from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker


class CatchLuredPokemon(BaseTask):
Expand All @@ -12,39 +13,52 @@ class CatchLuredPokemon(BaseTask):
def work(self):
lured_pokemon = self.get_lured_pokemon()
if lured_pokemon:
self.catch_pokemon(lured_pokemon)
for pokemon in lured_pokemon:
self.catch_pokemon(pokemon)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if your pokebag gets full while catching pokemon?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it just skips the catch, like it does already


def get_lured_pokemon(self):
forts_in_range = []
pokemon_to_catch = []
forts = self.bot.get_forts(order_by_distance=True)

if len(forts) == 0:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like a big win is limiting this API request to only run on near enough forts. Another win would be caching these lookups so we don't make these requests every tick anyways. I wonder if doing that (here and in more places) would be enough that we wouldn't need to do this internal loop that breaks the model we have had about the tick.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What we need is an ORM-like lib to avoid doing any manual call using the raw api object. Otherwise we code lots of duplicated cache, like the one we already have for pokestops timers. Also caching all seen pokestops may have a very big impact in memory usage for long-running bots. A simple cache for all pokestops is not the way, we need something smarter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I'd rather us refactor a dumb cache to make it smarter, than break our model of what a tick does while we wait for a smart cache to come.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TheSavior yes... we will need a size-limited dict for forts with more data and not only the timer. Using a LRU policy should be good to keep the size limit.

return False

fort = forts[0]
details = fort_details(self.bot, fort_id=fort['id'],
latitude=fort['latitude'],
longitude=fort['longitude'])
fort_name = details.get('name', 'Unknown')
for fort in forts:
distance_to_fort = distance(
self.bot.position[0],
self.bot.position[1],
fort['latitude'],
fort['longitude']
)

encounter_id = fort.get('lure_info', {}).get('encounter_id', None)
if distance_to_fort < Constants.MAX_DISTANCE_FORT_IS_REACHABLE and encounter_id:
forts_in_range.append(fort)


encounter_id = fort.get('lure_info', {}).get('encounter_id', None)
for fort in forts_in_range:
details = fort_details(self.bot, fort_id=fort['id'],
latitude=fort['latitude'],
longitude=fort['longitude'])
fort_name = details.get('name', 'Unknown')
encounter_id = fort['lure_info']['encounter_id']

if encounter_id:
result = {
'encounter_id': encounter_id,
'fort_id': fort['id'],
'fort_name': u"{}".format(fort_name),
'latitude': fort['latitude'],
'longitude': fort['longitude']
}
pokemon_to_catch.append(result)

self.emit_event(
'lured_pokemon_found',
formatted='Lured pokemon at fort {fort_name} ({fort_id})',
data=result
)
return result

return False
return pokemon_to_catch

def catch_pokemon(self, pokemon):
worker = PokemonCatchWorker(pokemon, self.bot)
Expand Down
23 changes: 19 additions & 4 deletions pokemongo_bot/cell_workers/catch_visible_pokemon.py
Expand Up @@ -17,12 +17,14 @@ def work(self):
lambda x: distance(self.bot.position[0], self.bot.position[1], x['latitude'], x['longitude'])
)
user_web_catchable = 'web/catchable-{}.json'.format(self.bot.config.username)


for pokemon in self.bot.cell['catchable_pokemons']:
with open(user_web_catchable, 'w') as outfile:
json.dump(pokemon, outfile)
self.emit_event(
'catchable_pokemon',
level='debug',
level='info',
data={
'pokemon_id': pokemon['pokemon_id'],
'spawn_point_id': pokemon['spawn_point_id'],
Expand All @@ -32,16 +34,29 @@ def work(self):
'expiration_timestamp_ms': pokemon['expiration_timestamp_ms'],
}
)

return self.catch_pokemon(self.bot.cell['catchable_pokemons'].pop(0))
self.catch_pokemon(pokemon)

if 'wild_pokemons' in self.bot.cell and len(self.bot.cell['wild_pokemons']) > 0:
# Sort all by distance from current pos- eventually this should
# build graph & A* it
self.bot.cell['wild_pokemons'].sort(
key=
lambda x: distance(self.bot.position[0], self.bot.position[1], x['latitude'], x['longitude']))
return self.catch_pokemon(self.bot.cell['wild_pokemons'].pop(0))

for pokemon in self.bot.cell['wild_pokemons']:
self.emit_event(
'catchable_pokemon',
level='info',
data={
'pokemon_id': pokemon['pokemon_data']['pokemon_id'],
'spawn_point_id': pokemon['spawn_point_id'],
'encounter_id': pokemon['encounter_id'],
'latitude': pokemon['latitude'],
'longitude': pokemon['longitude'],
'expiration_timestamp_ms': pokemon['time_till_hidden_ms'],
}
)
self.catch_pokemon(pokemon)

def catch_pokemon(self, pokemon):
worker = PokemonCatchWorker(pokemon, self.bot)
Expand Down
4 changes: 4 additions & 0 deletions pokemongo_bot/cell_workers/collect_level_up_reward.py
Expand Up @@ -12,6 +12,10 @@ def initialize(self):
self.previous_level = 0

def work(self):
if not self._time_to_run():
return False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should return SUCCESS

self._update_last_ran()

self.current_level = self._get_current_level()

# let's check level reward on bot initialization
Expand Down
5 changes: 5 additions & 0 deletions pokemongo_bot/cell_workers/evolve_pokemon.py
Expand Up @@ -25,6 +25,8 @@ def work(self):
if not self._should_run():
return

self._update_last_ran()

response_dict = self.api.get_inventory()
inventory_items = response_dict.get('responses', {}).get('GET_INVENTORY', {}).get('inventory_delta', {}).get(
'inventory_items', {})
Expand All @@ -42,6 +44,9 @@ def work(self):
self._execute_pokemon_evolve(pokemon, candy_list, cache)

def _should_run(self):
if not self._time_to_run():
return False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here


if not self.evolve_all or self.evolve_all[0] == 'none':
return False

Expand Down
4 changes: 4 additions & 0 deletions pokemongo_bot/cell_workers/incubate_eggs.py
Expand Up @@ -21,6 +21,10 @@ def _process_config(self):
self.longer_eggs_first = self.config.get("longer_eggs_first", True)

def work(self):
if not self._time_to_run():
return False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here

self._update_last_ran()

try:
self._check_inventory()
except:
Expand Down
9 changes: 5 additions & 4 deletions pokemongo_bot/cell_workers/move_to_fort.py
Expand Up @@ -13,17 +13,18 @@ class MoveToFort(BaseTask):

def initialize(self):
self.lure_distance = 0
self.lure_attraction = True #self.config.get("lure_attraction", True)
self.lure_max_distance = 2000 #self.config.get("lure_max_distance", 2000)
self.lure_attraction = self.config.get("lure_attraction", True)
self.lure_max_distance = self.config.get("lure_max_distance", 2000)
self.ignore_item_count = self.config.get("ignore_item_count", False)

def should_run(self):
has_space_for_loot = self.bot.has_space_for_loot()
if not has_space_for_loot:
self.emit_event(
'inventory_full',
formatted="Not moving to any forts as there aren't enough space. You might want to change your config to recycle more items if this message appears consistently."
formatted="Inventory is full. You might want to change your config to recycle more items if this message appears consistently."
)
return has_space_for_loot or self.bot.softban
return has_space_for_loot or self.ignore_item_count or self.bot.softban

def is_attracted(self):
return (self.lure_distance > 0)
Expand Down
5 changes: 5 additions & 0 deletions pokemongo_bot/cell_workers/nickname_pokemon.py
@@ -1,6 +1,7 @@
from pokemongo_bot.human_behaviour import sleep
from pokemongo_bot.base_task import BaseTask


class NicknamePokemon(BaseTask):
SUPPORTED_TASK_API_VERSION = 1

Expand All @@ -10,6 +11,10 @@ def initialize(self):
self.template = ""

def work(self):
if not self._time_to_run():
return False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here too

self._update_last_ran()

try:
inventory = reduce(dict.__getitem__, ["responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], self.bot.get_inventory())
except KeyError:
Expand Down
2 changes: 1 addition & 1 deletion pokemongo_bot/cell_workers/pokemon_catch_worker.py
Expand Up @@ -380,7 +380,7 @@ def work(self, response_dict=None):
data={'pokemon': pokemon_name}
)
break
time.sleep(5)
time.sleep(2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wonder the impact of this. It was originally 5 because that is how long the app takes. Changing it to two would make it faster, but would also not match what is possible. Perhaps making this configurable would also have a large enough impact on the tick loop that we wouldn't need to have internal loops in the tasks that break our model of how tick works.


def count_pokemon_inventory(self):
# don't use cached bot.get_inventory() here
Expand Down
4 changes: 4 additions & 0 deletions pokemongo_bot/cell_workers/recycle_items.py
Expand Up @@ -18,6 +18,10 @@ def _validate_item_filter(self):
raise ConfigException("item {} does not exist, spelling mistake? (check for valid item names in data/items.json)".format(config_item_name))

def work(self):
if not self._time_to_run():
return False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another one

self._update_last_ran()

self.bot.latest_inventory = None
item_count_dict = self.bot.item_inventory_count('all')

Expand Down