From 6e99169d963bd8637dfb55426650f2fe15e694bd Mon Sep 17 00:00:00 2001 From: Steffan Long Date: Sun, 24 Jul 2016 22:54:26 -0700 Subject: [PATCH 001/202] remove file exist checks to fix webview, remove gmaps api key write to html (#730) --- pokecli.py | 16 +++++++------- pokemongo_bot/__init__.py | 21 ++++++++----------- .../cell_workers/initial_transfer_worker.py | 6 ++---- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/pokecli.py b/pokecli.py index 07cf746c06..963b96bf20 100755 --- a/pokecli.py +++ b/pokecli.py @@ -48,6 +48,7 @@ def init_config(): parser = argparse.ArgumentParser() config_file = "config.json" release_config_json = "release_config.json" + web_dir = "web" # If config file exists, load variables from json load = {} @@ -172,15 +173,12 @@ def init_config(): with open(release_config_json) as data: config.release_config.update(json.load(data)) - web_index = 'web/index.html' - if config.gmapkey and os.path.isfile(web_index): - find_url = 'https:\/\/maps.googleapis.com\/maps\/api\/js\?key=\S*' - replace_url = "https://maps.googleapis.com/maps/api/js?key=%s&callback=initMap\"" - #Someone make this pretty! (Efficient) - with open(web_index, "r+") as sources: # r+ is read + write - lines = sources.readlines() - for line in lines: - sources.write(re.sub(r"%s" % find_url, replace_url % config.gmapkey, line)) + # create web dir if not exists + try: + os.makedirs(web_dir) + except OSError: + if not os.path.isdir(web_dir): + raise if config.evolve_all: config.evolve_all = [str(pokemon_name) for pokemon_name in config.evolve_all.split(',')] diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index d6df1a38d7..0c3acd1d8a 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -import os import logging import googlemaps import json @@ -57,15 +56,14 @@ def work_on_cell(self, cell, position, include_fort_on_path): lambda x: distance(self.position[0], self.position[1], x['latitude'], x['longitude'])) user_web_catchable = 'web/catchable-%s.json' % (self.config.username) - if os.path.isfile(user_web_catchable): # only write to file if it exists - for pokemon in cell['catchable_pokemons']: - with open(user_web_catchable, 'w') as outfile: - json.dump(pokemon, outfile) + for pokemon in cell['catchable_pokemons']: + with open(user_web_catchable, 'w') as outfile: + json.dump(pokemon, outfile) - if self.catch_pokemon(pokemon) == PokemonCatchWorker.NO_POKEBALLS: - break - with open(user_web_catchable, 'w') as outfile: - json.dump({}, outfile) + if self.catch_pokemon(pokemon) == PokemonCatchWorker.NO_POKEBALLS: + break + with open(user_web_catchable, 'w') as outfile: + json.dump({}, outfile) if (self.config.mode == "all" or self.config.mode == "poke" ) and 'wild_pokemons' in cell and len(cell['wild_pokemons']) > 0: @@ -237,9 +235,8 @@ def pokeball_inventory(self): 'inventory_delta']['inventory_items'] user_web_inventory = 'web/inventory-%s.json' % (self.config.username) - if os.path.isfile(user_web_inventory): - with open(user_web_inventory, 'w') as outfile: - json.dump(inventory_dict, outfile) + with open(user_web_inventory, 'w') as outfile: + json.dump(inventory_dict, outfile) # get player balls stock # ---------------------- diff --git a/pokemongo_bot/cell_workers/initial_transfer_worker.py b/pokemongo_bot/cell_workers/initial_transfer_worker.py index f09b91e07c..026e79ddaa 100644 --- a/pokemongo_bot/cell_workers/initial_transfer_worker.py +++ b/pokemongo_bot/cell_workers/initial_transfer_worker.py @@ -1,5 +1,4 @@ import json -import os from pokemongo_bot.human_behaviour import sleep from pokemongo_bot import logger @@ -51,9 +50,8 @@ def _initial_transfer_get_groups(self): 'inventory_delta']['inventory_items'] user_web_inventory = 'web/inventory-%s.json' % (self.config.username) - if os.path.isfile(user_web_inventory): - with open(user_web_inventory, 'w') as outfile: - json.dump(inventory_dict, outfile) + with open(user_web_inventory, 'w') as outfile: + json.dump(inventory_dict, outfile) for pokemon in inventory_dict: try: From 32cefeed4760d6b5d2b3eb76d783d0f13d068e83 Mon Sep 17 00:00:00 2001 From: TheGoldenXY Date: Mon, 25 Jul 2016 13:57:47 +0200 Subject: [PATCH 002/202] Update README.md (#782) --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 77c4a53af8..c56f1da784 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ You need modify config.json (config.json.example for example) then pokecli.py -- Please clean up your old clone if you have issue, and following the [install instruction](https://github.com/PokemonGoF/PokemonGo-Bot#installation). ## About dev/master Branch -Dev branch has most up to date feature and even everyone handle the part well, still, will have broken changes. Your test contribute and PR for fix are warm welcome. +Dev branch has the most up-to-date features, but be aware that there might be some broken changes. Your contribution and PR for fixes are warm welcome. Master branch is the stable branch. No PR on master branch to keep things easier. ## Table of Contents @@ -45,7 +45,7 @@ No PR on master branch to keep things easier. * Limit the step to farm specific area for pokestops * Use the ball you have to catch, don't if you don't have * Rudimentary IV Functionality filter - * Auto switch mode(Full of item then catch, no ball useable then farm) + * Auto switch mode (Full of item then catch, no ball useable then farm) * Ignore certain pokemon filter * Use superior ball types when necessary * When out of normal pokeballs, use the next type of ball unless there are less than 10 of that type, in which case switch to farm mode @@ -82,10 +82,10 @@ No PR on master branch to keep things easier. - Linux: `apt-get install python-protobuf` ### Note on branch -Please keep in mind that master is not always up to date whereas 'dev' is. In the installation note below change `master` to `dev` if you want to get the latest version. +Please keep in mind that master is not always up-to-date whereas 'dev' is. In the installation note below change `master` to `dev` if you want to get and use the latest version. ### Installation Linux -(change master to dev for the newer version) +(change master to dev for the latest version) ``` $ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot @@ -96,7 +96,7 @@ $ git submodule update ``` ### Installation Mac -(change master to dev for the newer version) +(change master to dev for the latest version) ``` $ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot @@ -109,7 +109,7 @@ $ git submodule update ``` ### Installation Windows -(change master to dev for the newer version) +(change master to dev for the latest version) On Windows, you will need to install PyYaml through the installer and not through requirements.txt. @@ -128,7 +128,7 @@ $ pip install PyYAML-3.11-cp27-cp27m-win32.whl // (replace PyYAML-3.11-cp27-cp27m-win32.whl with PyYAML-3.11-cp27-cp27m-win_amd64.whl ``` -After this, just do : +After this, just do: ``` $ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot @@ -164,20 +164,20 @@ This project uses Google Maps. There's one map coupled with the project, but as 6. After the code done, will update here how to replace. ### Python possible bug -If you encounter problems with the module `ssl` and it function `_create_unverified_context`. Just comment it. (Solution available in Python 2.7.11) -To do it follow instruction below : +If you encounter problems with the module `ssl` and it's function `_create_unverified_context`, just comment it. (Solution available in Python 2.7.11) +In order to comment out the function and the module, please follow the instructions below: - edit `pokecli.py` - put `#` before `if` (line 43) and `ssl` (line 44) - save it -Please keep in mind that this fix is necessary only if your python version don't have the `_create_unverified_context` argument in ssl module. +Please keep in mind that this fix is only necessary if your python version don't have the `_create_unverified_context` argument in the ssl module. ## Update To update your project do: `git pull` in the project folder -## Usage (up to date) +## Usage (up-to-date) 1/ copy `config.json.example` to `config.json` and `release_config.json.example` to `release_config.json`. - 2/ Edit `config.json` and replace `auth_service`, `username`, `password`, `location` and `gmapkey` with your parameters (others keys are optional, check `Advance Configuration` below) + 2/ Edit `config.json` and replace `auth_service`, `username`, `password`, `location` and `gmapkey` with your parameters (other keys are optional, check `Advance Configuration` below) ## Advance Configuration - `max_steps` : @@ -189,7 +189,7 @@ To update your project do: `git pull` in the project folder - `location_cache` : - `distance_unit` : - `item_filter` : -- `evolve_all` : Set to true to evolve pokemon if possible +- `evolve_all` : Set to true to evolve pokemons if possible ### Evolve All Configuration By setting the `evolve_all` attribute in config.json, you can instruct the bot to automatically @@ -264,7 +264,7 @@ Try to generate an [app password](!https://support.google.com/accounts/answer/18 ``` -p "" ``` -This error is mostly occurs for those who using 2 factor authentication but either way for the purpose of security would be nice to have a separate password for the bot app. +This error mostly occurs for those who are using 2 factor authentication, but either way, for the purpose of security it would be nice to have a separate password for the bot app. ### FLEE @@ -282,7 +282,7 @@ Create the following filter ``` ./data/catch-ignore.yml ``` -Its a yaml file with a list of names so make it look like +It's a yaml file with a list of names so make it look like ``` ignore: - Pidgey From 3aca6d798f8a3e173dce0beb7b13c9505cc5b251 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Mon, 25 Jul 2016 19:10:32 -0700 Subject: [PATCH 003/202] Update README.md (#896) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c56f1da784..0c05adbeed 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ # PokemonGo-Bot The Pokemon Go Bot, baking with community. - +## Help Needed on [Desktop Version](https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) ## Project Chat We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) ## Breaking Changes From 893483778366075aa9f3b80e090b448088439b93 Mon Sep 17 00:00:00 2001 From: Maestro Date: Wed, 27 Jul 2016 00:23:42 +0200 Subject: [PATCH 004/202] Linux/Mac needs sudo (#924) --- README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0c05adbeed..4e9d6c014d 100644 --- a/README.md +++ b/README.md @@ -71,15 +71,19 @@ No PR on master branch to keep things easier. - [Python 2.7.x](http://docs.python-guide.org/en/latest/starting/installation/) - [pip](https://pip.pypa.io/en/stable/installing/) - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Optional) +- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) - [docker](https://docs.docker.com/engine/installation/) (Optional) - [protobuf 3](https://github.com/google/protobuf) (OS Dependent, see below) +### Note on virtualenv +We recommend you use virtualenv, not only will this tool keep your OS clean from all the python plugins. +It also provide an virtual space for more than 1 instance! + ### Protobuf 3 installation - OS X: `brew update && brew install --devel protobuf` - Windows: Download protobuf 3.0: [here](https://github.com/google/protobuf/releases/download/v3.0.0-beta-4/protoc-3.0.0-beta-4-win32.zip) and unzip `bin/protoc.exe` into a folder in your PATH. -- Linux: `apt-get install python-protobuf` +- Linux: `sudo apt-get install python-protobuf` ### Note on branch Please keep in mind that master is not always up-to-date whereas 'dev' is. In the installation note below change `master` to `dev` if you want to get and use the latest version. @@ -89,7 +93,9 @@ Please keep in mind that master is not always up-to-date whereas 'dev' is. In th ``` $ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot -$ cd PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ source bin/activate $ pip install -r requirements.txt $ git submodule init $ git submodule update @@ -98,6 +104,9 @@ $ git submodule update ### Installation Mac (change master to dev for the latest version) +Make sure you install the following first: +[Requirements](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation) + ``` $ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot $ cd PokemonGo-Bot @@ -132,7 +141,9 @@ After this, just do: ``` $ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot -$ cd PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ source bin/activate $ pip install -r requirements.txt $ git submodule init $ git submodule update From 9ea015e1fd9c5d269276de8a8e822ee8eb0afa0f Mon Sep 17 00:00:00 2001 From: Linda_pp Date: Wed, 27 Jul 2016 13:58:26 +0900 Subject: [PATCH 005/202] Fix pgoapi package repository hash (#1117) because latest one breaks this bot --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0eb44fb82c..5c5223bf71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ --e git+https://github.com/tejado/pgoapi.git#egg=pgoapi +-e git+https://github.com/tejado/pgoapi.git@3787ffbe2e80ebce8a02d48eebceb9edf40179c1#egg=pgoapi geopy==1.11.0 protobuf==3.0.0b4 requests==2.10.0 From 0ce208e8f7976ea0bfcdf6f34978d9411a5e7138 Mon Sep 17 00:00:00 2001 From: Kenneth Cochran Date: Wed, 27 Jul 2016 19:33:19 -0500 Subject: [PATCH 006/202] changed line from tabs to spaces (#1294) (#1297) --- pokemongo_bot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 0c3acd1d8a..6dc13bbbda 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -82,7 +82,7 @@ def work_on_cell(self, cell, position, include_fort_on_path): forts = [fort for fort in cell['forts'] if 'latitude' in fort and 'type' in fort] - gyms = [gym for gym in cell['forts'] if 'gym_points' in gym] + gyms = [gym for gym in cell['forts'] if 'gym_points' in gym] # Sort all by distance from current pos- eventually this should # build graph & A* it From c7ab5b03cb93b5393bebbc383545a50e8519e57a Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Wed, 27 Jul 2016 21:58:25 -0700 Subject: [PATCH 007/202] Update README.md (#1332) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4e9d6c014d..dc6991af6e 100644 --- a/README.md +++ b/README.md @@ -365,3 +365,5 @@ Bitcoin Address: 1PJMCx9NNQRasQYaa4MMff9yyNFffhHgLu

+ +[![Analytics](https://ga-beacon.appspot.com/UA-81468120-1/welcome-page-master)](https://github.com/igrigorik/ga-beacon) From ec2b7add95da6bc580035d8d4c472c1d41e439a6 Mon Sep 17 00:00:00 2001 From: Eli White Date: Thu, 28 Jul 2016 10:36:48 -0700 Subject: [PATCH 008/202] Adding messaging to the readme about gym captures --- README.md | 74 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index dc6991af6e..ad9dd3b39b 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,13 @@ The Pokemon Go Bot, baking with community. ## Project Chat We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) ## Breaking Changes -You need modify config.json (config.json.example for example) then pokecli.py --config config.json +You need modify config.json (config.json.example for example) then pokecli.py --config config.json Please clean up your old clone if you have issue, and following the [install instruction](https://github.com/PokemonGoF/PokemonGo-Bot#installation). ## About dev/master Branch -Dev branch has the most up-to-date features, but be aware that there might be some broken changes. Your contribution and PR for fixes are warm welcome. -Master branch is the stable branch. -No PR on master branch to keep things easier. +Dev branch has the most up-to-date features, but be aware that there might be some broken changes. Your contribution and PR for fixes are warm welcome. +Master branch is the stable branch. +No PR on master branch to keep things easier. ## Table of Contents - [Project Chat](#project-chat) - [Features](#features) @@ -62,7 +62,9 @@ No PR on master branch to keep things easier. - [ ] Hatch eggs - [ ] Incubate eggs - [ ] Use candy -- [ ] Fight Gym + +## Gym Battles +This bot takes a strong stance against automating gym battles. Botting gyms will have a negative effect on most players and thus the game as a whole. We will thus never accept contributions or changes containing code specific for gym battles. ## Installation @@ -75,9 +77,9 @@ No PR on master branch to keep things easier. - [docker](https://docs.docker.com/engine/installation/) (Optional) - [protobuf 3](https://github.com/google/protobuf) (OS Dependent, see below) -### Note on virtualenv +### Note on virtualenv We recommend you use virtualenv, not only will this tool keep your OS clean from all the python plugins. -It also provide an virtual space for more than 1 instance! +It also provide an virtual space for more than 1 instance! ### Protobuf 3 installation @@ -92,7 +94,7 @@ Please keep in mind that master is not always up-to-date whereas 'dev' is. In th (change master to dev for the latest version) ``` -$ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot +$ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot $ cd PokemonGo-Bot $ virtualenv . $ source bin/activate @@ -108,10 +110,10 @@ Make sure you install the following first: [Requirements](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation) ``` -$ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot -$ cd PokemonGo-Bot -$ virtualenv . -$ source bin/activate +$ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ source bin/activate $ pip install -r requirements.txt $ git submodule init $ git submodule update @@ -126,7 +128,7 @@ On Windows, you will need to install PyYaml through the installer and not throug Go to : http://pyyaml.org/wiki/PyYAML , download the right version for your pc and install it ##### Windows 10: -Go to [this](http://www.lfd.uci.edu/~gohlke/pythonlibs/#pyyaml) page and download: PyYAML-3.11-cp27-cp27m-win32.whl +Go to [this](http://www.lfd.uci.edu/~gohlke/pythonlibs/#pyyaml) page and download: PyYAML-3.11-cp27-cp27m-win32.whl (If running 64-bit python or if you get a 'not a supported wheel on this platform' error, download the 64 bit version instead: PyYAML-3.11-cp27-cp27m-win_amd64.whl ) @@ -140,9 +142,9 @@ $ pip install PyYAML-3.11-cp27-cp27m-win32.whl After this, just do: ``` -$ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot -$ cd PokemonGo-Bot -$ virtualenv . +$ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . $ source bin/activate $ pip install -r requirements.txt $ git submodule init @@ -152,11 +154,11 @@ $ git submodule update ### Develop PokemonGo-Bot ``` -$ git clone -b dev https://github.com/PokemonGoF/PokemonGo-Bot -$ cd PokemonGo-Bot -$ virtualenv . -$ source bin/activate -$ pip install -r requirements.txt +$ git clone -b dev https://github.com/PokemonGoF/PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ source bin/activate +$ pip install -r requirements.txt $ git submodule init $ git submodule update ``` @@ -197,7 +199,7 @@ To update your project do: `git pull` in the project folder - `debug` : Let the default value here except if you are developper - `test` : Let the default value here except if you are developper - `initial_transfer` : Set this to 1 if you want to transfer pokemon -- `location_cache` : +- `location_cache` : - `distance_unit` : - `item_filter` : - `evolve_all` : Set to true to evolve pokemons if possible @@ -206,12 +208,12 @@ To update your project do: `git pull` in the project folder By setting the `evolve_all` attribute in config.json, you can instruct the bot to automatically evolve specified pokemons on startup. This is especially useful for batch-evolving after popping up a lucky egg (currently this needs to be done manually). - + The evolve all mechanism evolves only higher CP pokemons. It does this by first ordering them from high-to-low CP. It will also automatically transfer the evolved pokemons based on the release configuration. - + Examples on how to use (set in config.json): - + 1. "evolve_all": "all" Will evolve ALL pokemons. 2. "evolve_all": "Pidgey,Weedle" @@ -221,23 +223,23 @@ To update your project do: `git pull` in the project folder ## How to run with Docker ## How to add/discover new API - The example is [here](https://github.com/PokemonGoF/PokemonGo-Bot/commit/46e2352ce9f349cc127a408959679282f9999585) - 1. Check the type of your API request in [POGOProtos](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/RequestType.proto) For example: RECYCLE_INVENTORY_ITEM + The example is [here](https://github.com/PokemonGoF/PokemonGo-Bot/commit/46e2352ce9f349cc127a408959679282f9999585) + 1. Check the type of your API request in [POGOProtos](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/RequestType.proto) For example: RECYCLE_INVENTORY_ITEM 2. Convert to the api call in pokemongo_bot/__init__.py, RECYCLE_INVENTORY_ITEM change to self.api.recycle_inventory_item ``` def drop_item(self,item_id,count): self.api.recycle_inventory_item(...............) ``` - 3. Where is the param list? + 3. Where is the param list? You need check this [Requests/Messages/RecycleInventoryItemMessage.proto](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/Messages/RecycleInventoryItemMessage.proto) - 4. Then our final api call is + 4. Then our final api call is ``` def drop_item(self,item_id,count): self.api.recycle_inventory_item(item_id=item_id,count=count) inventory_req = self.api.call() print(inventory_req) - ``` - 5. You can now debug on the log to see if get what you need + ``` + 5. You can now debug on the log to see if get what you need ## How to set up a simple webserver with nginx ### Nginx on Ubuntu 14.x, 16.x @@ -304,15 +306,15 @@ ignore: - Zubat ``` ### How do I use the map?? -You can either view the map via opening the html file, or by serving it with SimpleHTTPServer (runs on localhost:8000) -To use SimpleHTTPServer: +You can either view the map via opening the html file, or by serving it with SimpleHTTPServer (runs on localhost:8000) +To use SimpleHTTPServer: ```$ python -m SimpleHTTPServer [port]``` The default port is 8080, you can change that by giving a port number. Anything above port 1000 does not require root. -You will need to set your username(s) in the userdata.js file before opening: +You will need to set your username(s) in the userdata.js file before opening: Copy userdata.js.example to userdata.js and edit with your favorite text editor. put your username in the quotes instead of "username" -If using multiple usernames format like this: +If using multiple usernames format like this: ```var users = ["username1","username2"];``` --------- @@ -348,7 +350,7 @@ If using multiple usernames format like this: * riberod07 * th3w4y * Leaklessgfy - + ------- ## Credits - [tejado](https://github.com/tejado) many thanks for the API From 3f1f8e321da1ee256659ef623730ee1efeb442d0 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Thu, 28 Jul 2016 22:02:59 -0700 Subject: [PATCH 009/202] Update README.md (#1542) --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index ad9dd3b39b..2fa29210c6 100644 --- a/README.md +++ b/README.md @@ -360,12 +360,4 @@ If using multiple usernames format like this: - [AHAAAAAAA](https://github.com/AHAAAAAAA/PokemonGo-Map) for parts of the s2sphere stuff -## Donation - -Bitcoin Address: 1PJMCx9NNQRasQYaa4MMff9yyNFffhHgLu - -

- -

- [![Analytics](https://ga-beacon.appspot.com/UA-81468120-1/welcome-page-master)](https://github.com/igrigorik/ga-beacon) From 03f1b428e8781b8c4aa35d1cd6a305fc13bbaff7 Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 29 Jul 2016 17:38:51 -0400 Subject: [PATCH 010/202] Delete pokemon_spawn_locations.json (#1704) **Short Description:** pokemon_spawn_locations.json is a file that I accidentally added to a previous PR, it doesn't have a purpose in the project at the moment. **Fixes:** - Remove pokemon_spawn_locations.json --- pokemon_spawn_locations.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 pokemon_spawn_locations.json diff --git a/pokemon_spawn_locations.json b/pokemon_spawn_locations.json deleted file mode 100644 index e39f6293e9..0000000000 --- a/pokemon_spawn_locations.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "Charmander": [42.48220496523662, -71.26985687706257] -} \ No newline at end of file From 55390cfd7fcedace99c2c3d372b3790ef5b7cfd7 Mon Sep 17 00:00:00 2001 From: Gregory Hanis Date: Sat, 30 Jul 2016 03:24:49 -0500 Subject: [PATCH 011/202] Update README.md (#1805) try now no conflict.. added features and shifted most over to wiki cleaned up some errors. added me to contrib list :) --- README.md | 248 ++++++------------------------------------------------ 1 file changed, 28 insertions(+), 220 deletions(-) diff --git a/README.md b/README.md index 2fa29210c6..b64e3c03b5 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,14 @@ # PokemonGo-Bot The Pokemon Go Bot, baking with community. ## Help Needed on [Desktop Version](https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) +## Help with the project [Dev Bot](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Develop-PokemonGo-Bot) ## Project Chat We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) ## Breaking Changes -You need modify config.json (config.json.example for example) then pokecli.py --config config.json -Please clean up your old clone if you have issue, and following the [install instruction](https://github.com/PokemonGoF/PokemonGo-Bot#installation). +You need modify config.json (config.json.pokemon.example for example) then python pokecli.py --config configs/config.json + +[More details about config file](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files) +Please clean up your old clone if you have issue, and following the [install instruction](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation). ## About dev/master Branch Dev branch has the most up-to-date features, but be aware that there might be some broken changes. Your contribution and PR for fixes are warm welcome. @@ -25,15 +28,7 @@ No PR on master branch to keep things easier. - [Project Chat](#project-chat) - [Features](#features) - [TODO List](#todo-list) -- __Installation__ - - [Requirements](#requirements) - - [Mac](#installation-mac) - - [Linux](#installation-linux) - - [Windows](#installation-windows) -- [Develop PokemonGo-Bot](develop-pokemonGo-bot) - [Usage](#usage) -- [Docker Usage](#how-to-run-with-docker) -- [FAQ](#faq) - [Credits](#credits) - [Donation](#donation) @@ -49,39 +44,33 @@ No PR on master branch to keep things easier. * Ignore certain pokemon filter * Use superior ball types when necessary * When out of normal pokeballs, use the next type of ball unless there are less than 10 of that type, in which case switch to farm mode - * Drop items when bag is full (In Testing, Document contribute needed) - * Pokemon catch filter (In Testing, Document contribute needed) - * Google Map API key setup (Readme update needed) + * Drop items + * Pokemon catch filter + * Google Map API key setup * Show all objects on map (In Testing) * Evolve pokemons (Code in, Need input, In Testing) + * Incubate eggs + * Hatch eggs + * Pokemon transfer filter ## TODO List - [ ] Standalone Desktop APP -- [ ] Pokemon transfer filter ?? This already done, right? -- [ ] Hatch eggs -- [ ] Incubate eggs - [ ] Use candy +- [ ] Softban Bypass (In Development) ## Gym Battles This bot takes a strong stance against automating gym battles. Botting gyms will have a negative effect on most players and thus the game as a whole. We will thus never accept contributions or changes containing code specific for gym battles. ## Installation - -### Requirements (click each one for install guide) - -- [Python 2.7.x](http://docs.python-guide.org/en/latest/starting/installation/) -- [pip](https://pip.pypa.io/en/stable/installing/) -- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) -- [docker](https://docs.docker.com/engine/installation/) (Optional) -- [protobuf 3](https://github.com/google/protobuf) (OS Dependent, see below) +[Getting started guide](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Getting-Started) +[Jump right into installing](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation) ### Note on virtualenv We recommend you use virtualenv, not only will this tool keep your OS clean from all the python plugins. It also provide an virtual space for more than 1 instance! -### Protobuf 3 installation +### Protobuf 3 installation Notes - OS X: `brew update && brew install --devel protobuf` - Windows: Download protobuf 3.0: [here](https://github.com/google/protobuf/releases/download/v3.0.0-beta-4/protoc-3.0.0-beta-4-win32.zip) and unzip `bin/protoc.exe` into a folder in your PATH. @@ -90,137 +79,17 @@ It also provide an virtual space for more than 1 instance! ### Note on branch Please keep in mind that master is not always up-to-date whereas 'dev' is. In the installation note below change `master` to `dev` if you want to get and use the latest version. -### Installation Linux -(change master to dev for the latest version) - -``` -$ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot -$ cd PokemonGo-Bot -$ virtualenv . -$ source bin/activate -$ pip install -r requirements.txt -$ git submodule init -$ git submodule update -``` - -### Installation Mac -(change master to dev for the latest version) - Make sure you install the following first: [Requirements](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation) -``` -$ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot -$ cd PokemonGo-Bot -$ virtualenv . -$ source bin/activate -$ pip install -r requirements.txt -$ git submodule init -$ git submodule update -``` - -### Installation Windows -(change master to dev for the latest version) - -On Windows, you will need to install PyYaml through the installer and not through requirements.txt. - -##### Windows vista, 7, 8: -Go to : http://pyyaml.org/wiki/PyYAML , download the right version for your pc and install it - -##### Windows 10: -Go to [this](http://www.lfd.uci.edu/~gohlke/pythonlibs/#pyyaml) page and download: PyYAML-3.11-cp27-cp27m-win32.whl -(If running 64-bit python or if you get a 'not a supported wheel on this platform' error, -download the 64 bit version instead: PyYAML-3.11-cp27-cp27m-win_amd64.whl ) - -``` -$ cd download-directory -$ pip install PyYAML-3.11-cp27-cp27m-win32.whl -// if you needed to download the 64-bit version) -// (replace PyYAML-3.11-cp27-cp27m-win32.whl with PyYAML-3.11-cp27-cp27m-win_amd64.whl -``` - -After this, just do: - -``` -$ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot -$ cd PokemonGo-Bot -$ virtualenv . -$ source bin/activate -$ pip install -r requirements.txt -$ git submodule init -$ git submodule update -``` - -### Develop PokemonGo-Bot - -``` -$ git clone -b dev https://github.com/PokemonGoF/PokemonGo-Bot -$ cd PokemonGo-Bot -$ virtualenv . -$ source bin/activate -$ pip install -r requirements.txt -$ git submodule init -$ git submodule update -``` - -### Google Maps API (in development) +### Google Maps API Bot Tracker +[Wiki on using the bot web folder](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page) -Google Maps API: a brief guide to your own key - -This project uses Google Maps. There's one map coupled with the project, but as it gets more popular we'll definitely hit the rate-limit making the map unusable. That said, here's how you can get your own and replace ours: - -1. Navigate to this [page](https://console.developers.google.com/flows/enableapi?apiid=maps_backend,geocoding_backend,directions_backend,distance_matrix_backend,elevation_backend,places_backend&keyType=CLIENT_SIDE&reusekey=true) -2. Select 'Create a project' in the dropdown menu. -3. Wait an eternity. -4. Click 'Create' on the next page (optionally, fill out the info) -5. Copy the API key that appears. -6. After the code done, will update here how to replace. - -### Python possible bug -If you encounter problems with the module `ssl` and it's function `_create_unverified_context`, just comment it. (Solution available in Python 2.7.11) -In order to comment out the function and the module, please follow the instructions below: -- edit `pokecli.py` -- put `#` before `if` (line 43) and `ssl` (line 44) -- save it - -Please keep in mind that this fix is only necessary if your python version don't have the `_create_unverified_context` argument in the ssl module. - -## Update -To update your project do: `git pull` in the project folder - -## Usage (up-to-date) - 1/ copy `config.json.example` to `config.json` and `release_config.json.example` to `release_config.json`. - 2/ Edit `config.json` and replace `auth_service`, `username`, `password`, `location` and `gmapkey` with your parameters (other keys are optional, check `Advance Configuration` below) - -## Advance Configuration -- `max_steps` : -- `mode` : -- `walk` : -- `debug` : Let the default value here except if you are developper -- `test` : Let the default value here except if you are developper -- `initial_transfer` : Set this to 1 if you want to transfer pokemon -- `location_cache` : -- `distance_unit` : -- `item_filter` : -- `evolve_all` : Set to true to evolve pokemons if possible - -### Evolve All Configuration - By setting the `evolve_all` attribute in config.json, you can instruct the bot to automatically - evolve specified pokemons on startup. This is especially useful for batch-evolving after popping up - a lucky egg (currently this needs to be done manually). - - The evolve all mechanism evolves only higher CP pokemons. It does this by first ordering them from high-to-low CP. - It will also automatically transfer the evolved pokemons based on the release configuration. - - Examples on how to use (set in config.json): - - 1. "evolve_all": "all" - Will evolve ALL pokemons. - 2. "evolve_all": "Pidgey,Weedle" - Will only evolve Pidgey and Weedle. - 3. Not setting evolve_all or having any other string would not evolve any pokemons on startup. +### FAQ +[Tips & Tricks](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ) ## How to run with Docker +[Wiki on how to use Docker](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/How-to-run-with-Docker) ## How to add/discover new API The example is [here](https://github.com/PokemonGoF/PokemonGo-Bot/commit/46e2352ce9f349cc127a408959679282f9999585) @@ -241,81 +110,19 @@ To update your project do: `git pull` in the project folder ``` 5. You can now debug on the log to see if get what you need -## How to set up a simple webserver with nginx -### Nginx on Ubuntu 14.x, 16.x -#### 1. Install nginx on your Ubuntu machine (e.g. on locally or AWS) -``` -sudo apt-get update -sudo apt-get install nginx -``` - -#### 2. Check the webserver -Check if the webserver is running by using your browser and entering the IP address of your local machine/server. -On a local machine this would be http://127.0.0.1. On AWS this is your public DNS if you havent configured an elastic IP. - -#### 3. Change Base Directory of the Webserver -``` -sudo nano "/etc/nginx/sites-enabled/default" -``` -Comment out following line: ```root /var/www/html;``` and change it to the web folder of your PokemonGo-Bot: eg: -``` -/home/user/dev/PokemonGo-Bot/web; -``` ## FAQ +[Wiki Link](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ) ### What's IV ? -Here's the [introduction](http://bulbapedia.bulbagarden.net/wiki/Individual_values) - -### Does it run automatally? -Not yet, still need a trainer to train the script param. But we are very close to. -### Set GEO Location -It works, use -l "xx.yyyy,zz.ttttt" to set lat long for location. -- diordache -### Google login issues (Login Error, Server busy)? - -Try to generate an [app password](!https://support.google.com/accounts/answer/185833?hl=en) and set is as -``` --p "" -``` -This error mostly occurs for those who are using 2 factor authentication, but either way, for the purpose of security it would be nice to have a separate password for the bot app. - +Here's the [introduction](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Pokemon-IV) +Research Website [Nice Tool](https://thesilphroad.com/research) -### FLEE -The status code "3" corresponds to "Flee" - meaning your Pokemon has ran away. - {"responses": { "CATCH_POKEMON": { "status": 3 } } -### My pokemon are not showing up in my Pokedex? -Finish the tutorial on a smartphone. This will then allow everything to be visible. -### How can I maximise my XP per hour? -Quick Tip: When using this script, use a Lucky egg to double the XP for 30 mins. You will level up much faster. A Lucky egg is obtained on level 9 and further on whilst leveling up. (from VipsForever via /r/pokemongodev) -### How can I not collect certain pokemon -You don't want to collect common pokemon once you hit a certain level. It will -slow down leveling but you won't fill up either. +### What are the Item ID +[Wiki](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Item-ID's) -Create the following filter -``` -./data/catch-ignore.yml -``` -It's a yaml file with a list of names so make it look like -``` -ignore: - - Pidgey - - Rattata - - Pidgeotto - - Spearow - - Ekans - - Zubat -``` -### How do I use the map?? -You can either view the map via opening the html file, or by serving it with SimpleHTTPServer (runs on localhost:8000) -To use SimpleHTTPServer: -```$ python -m SimpleHTTPServer [port]``` -The default port is 8080, you can change that by giving a port number. -Anything above port 1000 does not require root. -You will need to set your username(s) in the userdata.js file before opening: -Copy userdata.js.example to userdata.js and edit with your favorite text editor. -put your username in the quotes instead of "username" -If using multiple usernames format like this: -```var users = ["username1","username2"];``` +##Softban +[Wiki](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Softban) --------- ## Contributors (Don't forget add yours here when you create PR) @@ -350,6 +157,7 @@ If using multiple usernames format like this: * riberod07 * th3w4y * Leaklessgfy + * GregTampa ------- ## Credits From 78c5f2bc8c5aeafb902309583cfab621179bed4e Mon Sep 17 00:00:00 2001 From: Greg Bowyer Date: Sat, 30 Jul 2016 12:39:43 -0700 Subject: [PATCH 012/202] Tidy up readme.md a little (#1900) Silly markdown formatting --- README.md | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index b64e3c03b5..1a8af52065 100644 --- a/README.md +++ b/README.md @@ -92,24 +92,29 @@ Make sure you install the following first: [Wiki on how to use Docker](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/How-to-run-with-Docker) ## How to add/discover new API - The example is [here](https://github.com/PokemonGoF/PokemonGo-Bot/commit/46e2352ce9f349cc127a408959679282f9999585) - 1. Check the type of your API request in [POGOProtos](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/RequestType.proto) For example: RECYCLE_INVENTORY_ITEM - 2. Convert to the api call in pokemongo_bot/__init__.py, RECYCLE_INVENTORY_ITEM change to self.api.recycle_inventory_item - ``` - def drop_item(self,item_id,count): - self.api.recycle_inventory_item(...............) - ``` - 3. Where is the param list? - You need check this [Requests/Messages/RecycleInventoryItemMessage.proto](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/Messages/RecycleInventoryItemMessage.proto) - 4. Then our final api call is - ``` - def drop_item(self,item_id,count): - self.api.recycle_inventory_item(item_id=item_id,count=count) - inventory_req = self.api.call() - print(inventory_req) - ``` - 5. You can now debug on the log to see if get what you need +The example is [here](https://github.com/PokemonGoF/PokemonGo-Bot/commit/46e2352ce9f349cc127a408959679282f9999585) +1. Check the type of your API request in [POGOProtos](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/RequestType.proto) For example: RECYCLE_INVENTORY_ITEM + +2. Convert to the api call in pokemongo_bot/__init__.py, RECYCLE_INVENTORY_ITEM change to self.api.recycle_inventory_item + + ```python + def drop_item(self,item_id,count): + self.api.recycle_inventory_item(...............) + ``` + +3. Where is the param list? + You need check this [Requests/Messages/RecycleInventoryItemMessage.proto](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/Messages/RecycleInventoryItemMessage.proto) + +4. Then our final api call is + + ```python + def drop_item(self,item_id,count): + self.api.recycle_inventory_item(item_id=item_id,count=count) + inventory_req = self.api.call() + print(inventory_req) + ``` +5. You can now debug on the log to see if get what you need ## FAQ [Wiki Link](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ) From 4182e93f9bfbe471d07b74e95c55cdfbacda127b Mon Sep 17 00:00:00 2001 From: Max Kaplan Date: Sun, 31 Jul 2016 14:42:06 -0700 Subject: [PATCH 013/202] Add Python version to ISSUE_TEMPLATE.md (#1939) * Update ISSUE_TEMPLATE.md * Update ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 3091852955..9976991cf9 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -10,3 +10,4 @@ ### Other Information OS: Git Commit: (run 'git log -n 1 --pretty=format:"%H"' and paste it here) +Python Version: (run 'python -V' and paste it here) From 463bb1bc6d1c387c617f4cfca61737c26908448c Mon Sep 17 00:00:00 2001 From: JSchwerberg Date: Tue, 2 Aug 2016 10:48:03 -0700 Subject: [PATCH 014/202] Fix instance where evolve_all is unicode - fixes #2281 (#2305) * Fix instance where evolve_all is unicode * Test for isinstance basestring rather than Unicode || str --- pokemongo_bot/cell_workers/evolve_all.py | 349 +++++++++++++++++++++++ 1 file changed, 349 insertions(+) create mode 100644 pokemongo_bot/cell_workers/evolve_all.py diff --git a/pokemongo_bot/cell_workers/evolve_all.py b/pokemongo_bot/cell_workers/evolve_all.py new file mode 100644 index 0000000000..b23624ad92 --- /dev/null +++ b/pokemongo_bot/cell_workers/evolve_all.py @@ -0,0 +1,349 @@ +from pokemongo_bot import logger +from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.item_list import Item +from pokemongo_bot.cell_workers.base_task import BaseTask + +class EvolveAll(BaseTask): + def initialize(self): + self.evolve_all = self.config.get('evolve_all', []) + self.evolve_speed = self.config.get('evolve_speed', 3.7) + self.evolve_cp_min = self.config.get('evolve_cp_min', 300) + self.evolve_num_min = self.config.get('evolve_num_min', 5) + self.use_lucky_egg = self.config.get('use_lucky_egg', False) + self._validate_config() + + def _validate_config(self): + if isinstance(self.evolve_all, basestring): + self.evolve_all = [str(pokemon_name) for pokemon_name in self.evolve_all.split(',')] + + def work(self): + if not self._should_run(): + return + + response_dict = self.bot.get_inventory() + cache = {} + + try: + reduce(dict.__getitem__, [ + "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) + except KeyError: + pass + else: + inventory_items = response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] + candy_data = self._get_candy_data(inventory_items) + evolve_list = self._sort_by_cp_iv(inventory_items) + + # filter out non-listed pokemen and those with not enough candy + evolve_list = [x for x in evolve_list if self._is_evolvable(x, candy_data)] + + # Don't evolve unless the evolvable candidates number is no less than evolve_num_min + if len(evolve_list) < self.evolve_num_min: + # logger.log('Evolvable candidates number is {}, which is less than {}... skipping evolve.'.format( + # len(evolve_list), self.evolve_num_min), + # 'green') + return + + if self.use_lucky_egg: + lucky_egg_count = self.bot.item_inventory_count(Item.ITEM_LUCKY_EGG.value) + # Sometimes remaining lucky egg count get changed, check again for sure + if lucky_egg_count <= 0: + logger.log('No lucky eggs... skipping evolve!', 'yellow') + return + + logger.log('Using lucky egg ... you have {}'.format(lucky_egg_count)) + response_dict_lucky_egg = self.bot.use_lucky_egg() + if response_dict_lucky_egg and 'responses' in response_dict_lucky_egg and \ + 'USE_ITEM_XP_BOOST' in response_dict_lucky_egg['responses'] and \ + 'result' in response_dict_lucky_egg['responses']['USE_ITEM_XP_BOOST']: + result = response_dict_lucky_egg['responses']['USE_ITEM_XP_BOOST']['result'] + if result is 1: # Request success + logger.log('Successfully used lucky egg... ({} left!)'.format(lucky_egg_count - 1), 'green') + else: + logger.log('Failed to use lucky egg!', 'red') + return + + # enable to limit number of pokemons to evolve. Useful for testing. + # nn = 3 + # if len(evolve_list) > nn: + # evolve_list = evolve_list[:nn] + # + + id_list1 = self.count_pokemon_inventory() + for pokemon in evolve_list: + try: + self._execute_pokemon_evolve(pokemon, cache) + except Exception: + pass + id_list2 = self.count_pokemon_inventory() + release_cand_list_ids = list(set(id_list2) - set(id_list1)) + + if release_cand_list_ids: + logger.log('[#] Evolved {} pokemons! Checking if any of them needs to be released ...'.format( + len(release_cand_list_ids) + )) + self._release_evolved(release_cand_list_ids) + + def _should_run(self): + # Don't run after the first tick + # Lucky Egg should only be popped at the first tick + if not self.evolve_all or self.bot.tick_count is 1: + return False + + # Will skip evolving if user wants to use an egg and there is none + lucky_egg_count = self.bot.item_inventory_count(Item.ITEM_LUCKY_EGG.value) + if self.use_lucky_egg and lucky_egg_count <= 0: + logger.log('No lucky eggs... skipping evolve!', 'yellow') + return False + + # Otherwise try evolving + return True + + + def _release_evolved(self, release_cand_list_ids): + response_dict = self.bot.get_inventory() + cache = {} + + try: + reduce(dict.__getitem__, [ + "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) + except KeyError: + pass + else: + release_cand_list = self._sort_by_cp_iv( + response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']) + release_cand_list = [x for x in release_cand_list if x[0] in release_cand_list_ids] + + ## at this point release_cand_list contains evolved pokemons data + for cand in release_cand_list: + pokemon_id = cand[0] + pokemon_name = cand[1] + pokemon_cp = cand[2] + pokemon_potential = cand[3] + + if self.should_release_pokemon(pokemon_name, pokemon_cp, pokemon_potential): + # Transfering Pokemon + self.transfer_pokemon(pokemon_id) + logger.log( + '[#] {} has been exchanged for candy!'.format(pokemon_name), 'red') + + def _get_candy_data(self, inventory_items): + candy = {} + for item in inventory_items: + try: + reduce(dict.__getitem__, [ + "inventory_item_data", "candy"], item) + except KeyError: + pass + else: + try: + pokemon_candy = item['inventory_item_data']['candy'] + candy[pokemon_candy['family_id']] = pokemon_candy['candy'] + except Exception: + pass + return candy + + + def _sort_by_cp_iv(self, inventory_items): + pokemons1 = [] + pokemons2 = [] + for item in inventory_items: + try: + reduce(dict.__getitem__, [ + "inventory_item_data", "pokemon_data"], item) + except KeyError: + pass + else: + try: + pokemon = item['inventory_item_data']['pokemon_data'] + pokemon_num = int(pokemon['pokemon_id']) - 1 + pokemon_name = self.bot.pokemon_list[int(pokemon_num)]['Name'] + v = [ + pokemon['id'], + pokemon_name, + pokemon['cp'], + self._compute_iv(pokemon), + pokemon['pokemon_id'] + ] + if pokemon['cp'] > self.evolve_cp_min: + pokemons1.append(v) + else: + pokemons2.append(v) + except Exception: + pass + + # Sort larger CP pokemons by IV, tie breaking by CP + pokemons1.sort(key=lambda x: (x[3], x[2]), reverse=True) + + # Sort smaller CP pokemons by CP, tie breaking by IV + pokemons2.sort(key=lambda x: (x[2], x[3]), reverse=True) + + return pokemons1 + pokemons2 + + def _is_evolvable(self, pokemon, candy_data): + pokemon_name = pokemon[1] + family_id = pokemon[4] + # python list is index 0 based, thus - 1 + pokemon_idx = int(family_id) - 1 + + # Non-evolvable or top-tier pokemon + if 'Next Evolution Requirements' not in self.bot.pokemon_list[pokemon_idx]: + return False + + # filter out non-listed pokemen + if self.evolve_all[0] != 'all' and pokemon_name not in self.evolve_all: + return False + + # filter out those with not enough candy + if 'Previous evolution(s)' in self.bot.pokemon_list[pokemon_idx]: + family_id = int(self.bot.pokemon_list[pokemon_idx]['Previous evolution(s)'][0]['Number']) + + need_candies = int(self.bot.pokemon_list[pokemon_idx]['Next Evolution Requirements']['Amount']) + # print('{} need {} candies to evolve, currently have {}'. + # format(pokemon_name, need_candies, candy_data[family_id])) + if candy_data[family_id] >= need_candies: + candy_data[family_id] -= need_candies + return True + return False + + def _execute_pokemon_evolve(self, pokemon, cache): + pokemon_id = pokemon[0] + pokemon_name = pokemon[1] + pokemon_cp = pokemon[2] + pokemon_iv = pokemon[3] + + if pokemon_name in cache: + return + + self.bot.api.evolve_pokemon(pokemon_id=pokemon_id) + response_dict = self.bot.api.call() + status = response_dict['responses']['EVOLVE_POKEMON']['result'] + if status == 1: + logger.log('[#] Successfully evolved {} with {} CP and {} IV!'.format( + pokemon_name, pokemon_cp, pokemon_iv + )) + + sleep(self.evolve_speed) + + else: + # cache pokemons we can't evolve. Less server calls + cache[pokemon_name] = 1 + sleep(0.7) + + # 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() + + def count_pokemon_inventory(self): + response_dict = self.bot.get_inventory() + id_list = [] + return self.counting_pokemon(response_dict, id_list) + + def counting_pokemon(self, response_dict, id_list): + try: + reduce(dict.__getitem__, [ + "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) + except KeyError: + pass + else: + for item in response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']: + try: + reduce(dict.__getitem__, [ + "inventory_item_data", "pokemon_data"], item) + except KeyError: + pass + else: + pokemon = item['inventory_item_data']['pokemon_data'] + if pokemon.get('is_egg', False): + continue + id_list.append(pokemon['id']) + + return id_list + + def should_release_pokemon(self, pokemon_name, cp, iv): + if self._check_always_capture_exception_for(pokemon_name): + return False + else: + release_config = self._get_release_config_for(pokemon_name) + cp_iv_logic = release_config.get('logic') + if not cp_iv_logic: + cp_iv_logic = self._get_release_config_for('any').get('logic', 'and') + + release_results = { + 'cp': False, + 'iv': False, + } + + if 'release_below_cp' in release_config: + min_cp = release_config['release_below_cp'] + if cp < min_cp: + release_results['cp'] = True + + if 'release_below_iv' in release_config: + min_iv = release_config['release_below_iv'] + if iv < min_iv: + release_results['iv'] = True + + if release_config.get('always_release'): + return True + + logic_to_function = { + 'or': lambda x, y: x or y, + 'and': lambda x, y: x and y + } + + # logger.log( + # "[x] Release config for {}: CP {} {} IV {}".format( + # pokemon_name, + # min_cp, + # cp_iv_logic, + # min_iv + # ), 'yellow' + # ) + + return logic_to_function[cp_iv_logic](*release_results.values()) + + def _get_release_config_for(self, pokemon): + release_config = self.bot.config.release.get(pokemon) + if not release_config: + release_config = self.bot.config.release['any'] + return release_config + + def _get_exceptions(self): + exceptions = self.bot.config.release.get('exceptions') + if not exceptions: + return None + return exceptions + + def _get_always_capture_list(self): + exceptions = self._get_exceptions() + if not exceptions: + return [] + always_capture_list = exceptions['always_capture'] + if not always_capture_list: + return [] + return always_capture_list + + def _check_always_capture_exception_for(self, pokemon_name): + always_capture_list = self._get_always_capture_list() + if not always_capture_list: + return False + else: + for pokemon in always_capture_list: + if pokemon_name == str(pokemon): + return True + return False + + # TODO: should also go to util and refactor in catch worker + def _compute_iv(self, pokemon): + total_IV = 0.0 + iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] + + for individual_stat in iv_stats: + try: + total_IV += pokemon[individual_stat] + except Exception: + pokemon[individual_stat] = 0 + continue + pokemon_potential = round((total_IV / 45.0), 2) + return pokemon_potential From d7574b2ee4f2b5a7beb289561a7efd31a7a2b0f7 Mon Sep 17 00:00:00 2001 From: Zhou Date: Mon, 1 Aug 2016 12:46:11 -0700 Subject: [PATCH 015/202] Evolve only if evolvable candidates no less than a certain number. Previously, when user configured use_lucky_egg as true, the bot will use lucky egg even evolvale candidates is zero. That's kinda of waste. Meanwhile, if user didn't configured use_lucky_egg, the bot will try to evolve after sort by cp&iv, but sometimes no good potential pokemon in that batch, it's better delay the evolution after a while. By adding a config item evovle_num_min can cope above two problems. The bot evolves only if evolvable candidates no less than a certain number. 1. Evolve only if evolvable candidates no less than a certain number. 2. Dragoniar's configuration is abnormal in pokemon.json, which lack of "Previous evolution(s)" attribute, while the other middle-tier pokemon has. Fixed some issues after: Refactor evolve_all worker #2244 1. The refactoring try to sort by pokemon index desc which is not good. For example, why Venusaur which is #003 should be with lower priority than Pidgey(#016)? 2. The refactoring always use dict.get(key, {}).get(key, {}) which will eventually return a {} which will cause side effect. It's better fail fast when the dict has no attribute which is unexpected by the developer. 3. The reafctoring try to use cache when the pokemon is not evovlable. That developer doesn't know previously we didn't caculate the candy before evovling, thus caused a lot of failure when evovling. After caculating candy requirements by the refactoring and this patch, the failure rarely happens. Thus cache is not necessary when the pokemon is not evovlable. --- configs/config.json.cluster.example | 1 + configs/config.json.example | 1 + configs/config.json.path.example | 1 + configs/config.json.pokemon.example | 1 + data/pokemon.json | 2 +- pokemongo_bot/cell_workers/evolve_all.py | 349 ------------------- pokemongo_bot/cell_workers/evolve_pokemon.py | 205 ++++++----- 7 files changed, 129 insertions(+), 431 deletions(-) delete mode 100644 pokemongo_bot/cell_workers/evolve_all.py diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index 5613061a84..6a63efbae9 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -27,6 +27,7 @@ "first_evolve_by": "cp", "evolve_above_cp": 500, "evolve_above_iv": 0.8, + "evolve_num_min": 5, "logic": "or", "evolve_speed": 20, "use_lucky_egg": false diff --git a/configs/config.json.example b/configs/config.json.example index 20ef72e34e..82bad9eaac 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -27,6 +27,7 @@ "first_evolve_by": "cp", "evolve_above_cp": 500, "evolve_above_iv": 0.8, + "evolve_num_min": 5, "logic": "or", "evolve_speed": 20, "use_lucky_egg": false diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 38baa9f1f0..f8725c9eba 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -27,6 +27,7 @@ "first_evolve_by": "cp", "evolve_above_cp": 500, "evolve_above_iv": 0.8, + "evolve_num_min": 5, "logic": "or", "evolve_speed": 20, "use_lucky_egg": false diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 7cad1ac066..8f9a5ce98f 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -27,6 +27,7 @@ "first_evolve_by": "cp", "evolve_above_cp": 500, "evolve_above_iv": 0.8, + "evolve_num_min": 5, "logic": "or", "evolve_speed": 20, "use_lucky_egg": false diff --git a/data/pokemon.json b/data/pokemon.json index a227106841..9851c45aed 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -1 +1 @@ -[{"Number":"001","Name":"Bulbasaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Tackle","Vine Whip"],"Weight":"6.9 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"002","Name":"Ivysaur"},{"Number":"003","Name":"Venusaur"}]},{"Number":"002","Name":"Ivysaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"13.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"003","Name":"Venusaur"}]},{"Number":"003","Name":"Venusaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"100.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"},{"Number":"002","Name":"Ivysaur"}]},{"Number":"004","Name":"Charmander","Classification":"Lizard Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Scratch"],"Weight":"8.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"005","Name":"Charmeleon"},{"Number":"006","Name":"Charizard"}]},{"Number":"005","Name":"Charmeleon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"19.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"}],"Next Evolution Requirements":{"Amount":100,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"006","Name":"Charizard"}]},{"Number":"006","Name":"Charizard","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Ember","Wing Attack"],"Weight":"90.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"},{"Number":"005","Name":"Charmeleon"}]},{"Number":"007","Name":"Squirtle","Classification":"Tiny Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Tackle","Bubble"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"008","Name":"Wartortle"},{"Number":"009","Name":"Blastoise"}]},{"Number":"008","Name":"Wartortle","Classification":"Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"22.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"}],"Next Evolution Requirements":{"Amount":100,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"009","Name":"Blastoise"}]},{"Number":"009","Name":"Blastoise","Classification":"Shellfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"85.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"},{"Number":"008","Name":"Wartortle"}]},{"Number":"010","Name":"Caterpie","Classification":"Worm Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"2.9 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"011","Name":"Metapod"},{"Number":"012","Name":"Butterfree"}]},{"Number":"011","Name":"Metapod","Classification":"Cocoon Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"9.9 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"}],"Next Evolution Requirements":{"Amount":50,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"012","Name":"Butterfree"}]},{"Number":"012","Name":"Butterfree","Classification":"Butterfly Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"32.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"},{"Number":"011","Name":"Metapod"}]},{"Number":"013","Name":"Weedle","Classification":"Hairy Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Sting"],"Weight":"3.2 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"014","Name":"Kakuna"},{"Number":"015","Name":"Beedrill"}]},{"Number":"014","Name":"Kakuna","Classification":"Cocoon Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Posion Sting"],"Weight":"10.0 kg","Height":"0.6 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"}],"Next Evolution Requirements":{"Amount":50,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"015","Name":"Beedrill"}]},{"Number":"015","Name":"Beedrill","Classification":"Poison Bee Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Jab"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"},{"Number":"014","Name":"Kakuna"}]},{"Number":"016","Name":"Pidgey","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"1.8 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"017","Name":"Pidgeotto"},{"Number":"018","Name":"Pidgeot"}]},{"Number":"017","Name":"Pidgeotto","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"30.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"}],"Next Evolution Requirements":{"Amount":50,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"018","Name":"Pidgeot"}]},{"Number":"018","Name":"Pidgeot","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Hurricane"],"Weight":"39.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"},{"Number":"017","Name":"Pidgeotto"}]},{"Number":"019","Name":"Rattata","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Body Slam","Dig","Hyper Fang"],"Weight":"3.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Rattata candies"},"Next evolution(s)":[{"Number":"020","Name":"Raticate"}]},{"Number":"020","Name":"Raticate","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Quick Attack"],"Special Attack(s)":["Dig","Hyper Beam","Hyper Fang"],"Weight":"18.5 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"019","Name":"Rattata"}]},{"Number":"021","Name":"Spearow","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"2.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Spearow candies"},"Next evolution(s)":[{"Number":"022","Name":"Fearow"}]},{"Number":"022","Name":"Fearow","Classification":"Beak Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Steel Wing"],"Weight":"38.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"021","Name":"Spearow"}]},{"Number":"023","Name":"Ekans","Classification":"Snake Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Sting"],"Weight":"6.9 kg","Height":"2.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ekans candies"},"Next evolution(s)":[{"Number":"024","Name":"Arbok"}]},{"Number":"024","Name":"Arbok","Classification":"Cobra Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Bite"],"Weight":"65.0 kg","Height":"3.5 m","Previous evolution(s)":[{"Number":"023","Name":"Ekans"}]},{"Number":"025","Name":"Pikachu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Quick Attack","Thunder Shock"],"Weight":"6.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Pikachu candies"},"Next evolution(s)":[{"Number":"026","Name":"Raichu"}]},{"Number":"026","Name":"Raichu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock","Spark"],"Weight":"30.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"025","Name":"Pikachu"}]},{"Number":"027","Name":"Sandshrew","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"12.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Sandshrew candies"},"Next evolution(s)":[{"Number":"028","Name":"Sandslash"}]},{"Number":"028","Name":"Sandslash","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"027","Name":"Sandshrew"}]},{"Number":"029","Name":"Nidoran F","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"7.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"030","Name":"Nidorina"},{"Number":"031","Name":"Nidoqueen"}]},{"Number":"030","Name":"Nidorina","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"20.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"031","Name":"Nidoqueen"}]},{"Number":"031","Name":"Nidoqueen","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"},{"Number":"030","Name":"Nidorina"}]},{"Number":"032","Name":"Nidoran M","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Peck","Poison Sting"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"033","Name":"Nidorino"},{"Number":"034","Name":"Nidoking"}]},{"Number":"033","Name":"Nidorino","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"19.5 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"034","Name":"Nidoking"}]},{"Number":"034","Name":"Nidoking","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Fury Cutter","Poison Jab"],"Weight":"62.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"},{"Number":"033","Name":"Nidorino"}]},{"Number":"035","Name":"Clefairy","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"7.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Clefairy candies"},"Next evolution(s)":[{"Number":"036","Name":"Clefable"}]},{"Number":"036","Name":"Clefable","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"40.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"035","Name":"Clefairy"}]},{"Number":"037","Name":"Vulpix","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"9.9 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Vulpix candies"},"Next evolution(s)":[{"Number":"038","Name":"Ninetales"}]},{"Number":"038","Name":"Ninetales","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"19.9 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"037","Name":"Vulpix"}]},{"Number":"039","Name":"Jigglypuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"5.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Jigglypuff candies"},"Next evolution(s)":[{"Number":"039","Name":"Jigglypuff"}]},{"Number":"040","Name":"Wigglytuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"12.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"040","Name":"Wigglytuff"}]},{"Number":"041","Name":"Zubat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Quick Attack"],"Weight":"7.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Zubat candies"},"Next evolution(s)":[{"Number":"042","Name":"Golbat"}]},{"Number":"042","Name":"Golbat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Wing Attack"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"041","Name":"Zubat"}]},{"Number":"043","Name":"Oddish","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"5.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"044","Name":"Gloom"},{"Number":"045","Name":"Vileplume"}]},{"Number":"044","Name":"Gloom","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"8.6 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"}],"Next Evolution Requirements":{"Amount":100,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"045","Name":"Vileplume"}]},{"Number":"045","Name":"Vileplume","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid",""],"Weight":"18.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"},{"Number":"044","Name":"Gloom"}]},{"Number":"046","Name":"Paras","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Scratch"],"Weight":"5.4 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Paras candies"},"Next evolution(s)":[{"Number":"047","Name":"Parasect"}]},{"Number":"047","Name":"Parasect","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Fury Cutter"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"046","Name":"Paras"}]},{"Number":"048","Name":"Venonat","Classification":"Insect Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Venonat candies"},"Next evolution(s)":[{"Number":"049","Name":"Venomoth"}]},{"Number":"049","Name":"Venomoth","Classification":"Poison Moth Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"12.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"048","Name":"Venonat"}]},{"Number":"050","Name":"Diglett","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"0.8 kg","Height":"0.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Diglett candies"},"Next evolution(s)":[{"Number":"051","Name":"Dugtrio"}]},{"Number":"051","Name":"Dugtrio","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Sucker Punch"],"Weight":"33.3 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"050","Name":"Diglett"}]},{"Number":"052","Name":"Meowth","Classification":"Scratch Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Scratch"],"Weight":"4.2 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Meowth candies"},"Next evolution(s)":[{"Number":"053","Name":"Persian"}]},{"Number":"053","Name":"Persian","Classification":"Classy Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Scratch"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"052","Name":"Meowth"}]},{"Number":"054","Name":"Psyduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun","Zen Headbutt"],"Weight":"19.6 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Psyduck candies"},"Next evolution(s)":[{"Number":"055","Name":"Golduck"}]},{"Number":"055","Name":"Golduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"76.6 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"054","Name":"Psyduck"}]},{"Number":"056","Name":"Mankey","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Scratch"],"Weight":"28.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Mankey candies"},"Next evolution(s)":[{"Number":"057","Name":"Primeape"}]},{"Number":"057","Name":"Primeape","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"056","Name":"Mankey"}]},{"Number":"058","Name":"Growlithe","Classification":"Puppy Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Ember"],"Weight":"19.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":50,"Name":"Growlithe candies"},"Next evolution(s)":[{"Number":"059","Name":"Arcanine"}]},{"Number":"059","Name":"Arcanine","Classification":"Legendary Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Fire Fang"],"Weight":"155.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"058","Name":"Growlithe"}]},{"Number":"060","Name":"Poliwag","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"12.4 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"061","Name":"Poliwhirl"},{"Number":"062","Name":"Poliwrath"}]},{"Number":"061","Name":"Poliwhirl","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"20.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"}],"Next Evolution Requirements":{"Amount":100,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"062","Name":"Poliwrath"}]},{"Number":"062","Name":"Poliwrath","Classification":"Tadpole Pokemon","Type I":["Water"],"Type II":["Fighting"],"Weaknesses":["Electric","Grass","Flying","Psychic","Fairy"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"54.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"},{"Number":"061","Name":"Poliwhirl"}]},{"Number":"063","Name":"Abra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Zen Headbutt",""],"Weight":"19.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":25,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"064","Name":"Kadabra"},{"Number":"065","Name":"Alakazam"}]},{"Number":"064","Name":"Kadabra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"56.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"}],"Next Evolution Requirements":{"Amount":100,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"065","Name":"Alakazam"}]},{"Number":"065","Name":"Alakazam","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"48.0 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"},{"Number":"064","Name":"Kadabra"}]},{"Number":"066","Name":"Machop","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"19.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"067","Name":"Machoke"},{"Number":"068","Name":"Machamp"}]},{"Number":"067","Name":"Machoke","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"70.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"}],"Next Evolution Requirements":{"Amount":100,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"068","Name":"Machamp"}]},{"Number":"068","Name":"Machamp","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Karate Chop"],"Weight":"130.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"},{"Number":"067","Name":"Machoke"}]},{"Number":"069","Name":"Bellsprout","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Vine Whip"],"Weight":"4.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"070","Name":"Weepinbell"},{"Number":"071","Name":"Victreebel"}]},{"Number":"070","Name":"Weepinbell","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"6.4 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"071","Name":"Victreebel"}]},{"Number":"071","Name":"Victreebel","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"15.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"},{"Number":"070","Name":"Weepinbell"}]},{"Number":"072","Name":"Tentacool","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Bubble","Poison Sting"],"Weight":"45.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Tentacool candies"},"Next evolution(s)":[{"Number":"073","Name":"Tentacruel"}]},{"Number":"073","Name":"Tentacruel","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Jab"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"072","Name":"Tentacool"}]},{"Number":"074","Name":"Geodude","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"20.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"075","Name":"Graveler"},{"Number":"076","Name":"Golem"}]},{"Number":"075","Name":"Graveler","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"105.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"}],"Next Evolution Requirements":{"Amount":100,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"076","Name":"Golem"}]},{"Number":"076","Name":"Golem","Classification":"Megaton Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"300.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"},{"Number":"075","Name":"Graveler"}]},{"Number":"077","Name":"Ponyta","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Tackle"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ponyta candies"},"Next evolution(s)":[{"Number":"078","Name":"Rapidash"}]},{"Number":"078","Name":"Rapidash","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Low Kick"],"Weight":"95.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"077","Name":"Ponyta"}]},{"Number":"079","Name":"Slowpoke","Classification":"Dopey Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"36.0 kg","Height":"1.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Slowpoke candies"},"Next evolution(s)":[{"Number":"080","Name":"Slowbro"}]},{"Number":"080","Name":"Slowbro","Classification":"Hermit Crab Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"78.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"079","Name":"Slowpoke"}]},{"Number":"081","Name":"Magnemite","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"6.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Magnemite candies"},"Next evolution(s)":[{"Number":"082","Name":"Magneton"}]},{"Number":"082","Name":"Magneton","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"60.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"081","Name":"Magnemite"}]},{"Number":"083","Name":"Farfetch'd","Classification":"Wild Duck Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"15.0 kg","Height":"0.8 m"},{"Number":"084","Name":"Doduo","Classification":"Twin Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"39.2 kg","Height":"1.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Doduo candies"},"Next evolution(s)":[{"Number":"085","Name":"Dodrio"}]},{"Number":"085","Name":"Dodrio","Classification":"Triple Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Feint Attack","Steel Wing"],"Weight":"85.2 kg","Height":"1.8 m","Previous evolution(s)":[{"Number":"084","Name":"Doduo"}]},{"Number":"086","Name":"Seel","Classification":"Sea Lion Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Water Gun"],"Weight":"90.0 kg","Height":"1.1 m","Next Evolution Requirements":{"Amount":50,"Name":"Seel candies"},"Next evolution(s)":[{"Number":"087","Name":"Dewgong"}]},{"Number":"087","Name":"Dewgong","Classification":"Sea Lion Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"120.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"086","Name":"Seel"}]},{"Number":"088","Name":"Grimer","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Mud Slap"],"Weight":"30.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Grimer candies"},"Next evolution(s)":[{"Number":"089","Name":"Muk"}]},{"Number":"089","Name":"Muk","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Poison Jab",""],"Weight":"30.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"088","Name":"Grimer"}]},{"Number":"090","Name":"Shellder","Classification":"Bivalve Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Tackle"],"Weight":"4.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Shellder candies"},"Next evolution(s)":[{"Number":"091","Name":"Cloyster"}]},{"Number":"091","Name":"Cloyster","Classification":"Bivalve Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"132.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"090","Name":"Shellder"}]},{"Number":"092","Name":"Gastly","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Sucker Punch"],"Weight":"0.1 kg","Height":"1.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"093","Name":"Haunter"},{"Number":"094","Name":"Gengar"}]},{"Number":"093","Name":"Haunter","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Shadow Claw"],"Weight":"0.1 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"}],"Next Evolution Requirements":{"Amount":100,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"094","Name":"Gengar"}]},{"Number":"094","Name":"Gengar","Classification":"Shadow Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Shadow Claw","Sucker Punch"],"Weight":"40.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"},{"Number":"093","Name":"Haunter"}]},{"Number":"095","Name":"Onix","Classification":"Rock Snake Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"210.0 kg","Height":"8.8 m"},{"Number":"096","Name":"Drowzee","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Pound"],"Weight":"32.4 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Drowzee candies"},"Next evolution(s)":[{"Number":"097","Name":"Hypno"}]},{"Number":"097","Name":"Hypno","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"75.6 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"096","Name":"Drowzee"}]},{"Number":"098","Name":"Krabby","Classification":"River Crab Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Krabby candies"},"Next evolution(s)":[{"Number":"099","Name":"Kingler"}]},{"Number":"099","Name":"Kingler","Classification":"Pincer Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"098","Name":"Krabby"}]},{"Number":"100","Name":"Voltorb","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark","Tackle"],"Weight":"10.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Voltorb candies"},"Next evolution(s)":[{"Number":"101","Name":"Electrode"}]},{"Number":"101","Name":"Electrode","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark",""],"Weight":"66.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"100","Name":"Voltorb"}]},{"Number":"102","Name":"Exeggcute","Classification":"Egg Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion",""],"Weight":"2.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Exeggcute candies"},"Next evolution(s)":[{"Number":"103","Name":"Exeggutor"}]},{"Number":"103","Name":"Exeggutor","Classification":"Coconut Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"120.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"102","Name":"Exeggcute"}]},{"Number":"104","Name":"Cubone","Classification":"Lonely Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Cubone candies"},"Next evolution(s)":[{"Number":"105","Name":"Marowak"}]},{"Number":"105","Name":"Marowak","Classification":"Bone Keeper Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"45.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"104","Name":"Cubone"}]},{"Number":"106","Name":"Hitmonlee","Classification":"Kicking Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Low Kick","Rock Smash"],"Weight":"49.8 kg","Height":"1.5 m","Next evolution(s)":[{"Number":"107","Name":"Hitmonchan"}]},{"Number":"107","Name":"Hitmonchan","Classification":"Punching Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Rock Smash"],"Weight":"50.2 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"106","Name":"Hitmonlee"}]},{"Number":"108","Name":"Lickitung","Classification":"Licking Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"65.5 kg","Height":"1.2 m"},{"Number":"109","Name":"Koffing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"1.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Koffing candies"},"Next evolution(s)":[{"Number":"110","Name":"Weezing"}]},{"Number":"110","Name":"Weezing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"9.5 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"109","Name":"Koffing"}]},{"Number":"111","Name":"Rhyhorn","Classification":"Spikes Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"115.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Rhyhorn candies"},"Next evolution(s)":[{"Number":"112","Name":"Rhydon"}]},{"Number":"112","Name":"Rhydon","Classification":"Drill Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"120.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"111","Name":"Rhyhorn"}]},{"Number":"113","Name":"Chansey","Classification":"Egg Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"34.6 kg","Height":"1.1 m"},{"Number":"114","Name":"Tangela","Classification":"Vine Pokemon","Type I":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug"],"Fast Attack(s)":["Vine Whip",""],"Weight":"35.0 kg","Height":"1.0 m"},{"Number":"115","Name":"Kangaskhan","Classification":"Parent Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Low Kick",""],"Weight":"80.0 kg","Height":"2.2 m"},{"Number":"116","Name":"Horsea","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Water Gun"],"Weight":"8.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Horsea candies"},"Next evolution(s)":[{"Number":"117","Name":"Seadra"}]},{"Number":"117","Name":"Seadra","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Dragon Breath","Water Gun"],"Weight":"25.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"116","Name":"Horsea"}]},{"Number":"118","Name":"Goldeen","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Mud Shot"],"Weight":"15.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Goldeen candies"},"Next evolution(s)":[{"Number":"119","Name":"Seaking"}]},{"Number":"119","Name":"Seaking","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Poison Jab"],"Weight":"39.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"118","Name":"Goldeen"}]},{"Number":"120","Name":"Staryu","Classification":"Starshape Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"34.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Staryu candies"},"Next evolution(s)":[{"Number":"120","Name":"Staryu"}]},{"Number":"121","Name":"Starmie","Classification":"Mysterious Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"80.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"121","Name":"Starmie"}]},{"Number":"122","Name":"Mr. Mime","Classification":"Barrier Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"54.5 kg","Height":"1.3 m"},{"Number":"123","Name":"Scyther","Classification":"Mantis Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Steel Wing"],"Weight":"56.0 kg","Height":"1.5 m"},{"Number":"124","Name":"Jynx","Classification":"Humanshape Pokemon","Type I":["Ice"],"Type II":["Psychic"],"Weaknesses":["Fire","Bug","Rock","Ghost","Dark","Steel"],"Fast Attack(s)":["Frost Breath","Pound"],"Weight":"40.6 kg","Height":"1.4 m"},{"Number":"125","Name":"Electabuzz","Classification":"Electric Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Low Kick","Thunder Shock"],"Weight":"30.0 kg","Height":"1.1 m"},{"Number":"126","Name":"Magmar","Classification":"Spitfire Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Karate Chop"],"Weight":"44.5 kg","Height":"1.3 m"},{"Number":"127","Name":"Pinsir","Classification":"Stagbeetle Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Rock Smash"],"Weight":"55.0 kg","Height":"1.5 m"},{"Number":"128","Name":"Tauros","Classification":"Wild Bull Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Tackle","Zen Headbutt"],"Weight":"88.4 kg","Height":"1.4 m"},{"Number":"129","Name":"Magikarp","Classification":"Fish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Splash",""],"Weight":"10.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":400,"Name":"Magikarp candies"},"Next evolution(s)":[{"Number":"130","Name":"Gyarados"}]},{"Number":"130","Name":"Gyarados","Classification":"Atrocious Pokemon","Type I":["Water"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Bite","Dragon Breath"],"Weight":"235.0 kg","Height":"6.5 m","Previous evolution(s)":[{"Number":"129","Name":"Magikarp"}]},{"Number":"131","Name":"Lapras","Classification":"Transport Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"220.0 kg","Height":"2.5 m"},{"Number":"132","Name":"Ditto","Classification":"Transform Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.3 m"},{"Number":"133","Name":"Eevee","Classification":"Evolution Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"6.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Eevee candies"},"Next evolution(s)":[{"Number":"134","Name":"Vaporeon"},{"Number":"135","Name":"Jolteon"},{"Number":"136","Name":"Flareon"}]},{"Number":"134","Name":"Vaporeon","Classification":"Bubble Jet Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun",""],"Weight":"29.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"135","Name":"Jolteon","Classification":"Lightning Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock",""],"Weight":"24.5 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"136","Name":"Flareon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"25.0 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"137","Name":"Porygon","Classification":"Virtual Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"36.5 kg","Height":"0.8 m"},{"Number":"138","Name":"Omanyte","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Water Gun",""],"Weight":"7.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Omanyte candies"},"Next evolution(s)":[{"Number":"139","Name":"Omastar"}]},{"Number":"139","Name":"Omastar","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Rock Throw","Water Gun"],"Weight":"35.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"138","Name":"Omanyte"}]},{"Number":"140","Name":"Kabuto","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"11.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Kabuto candies"},"Next evolution(s)":[{"Number":"141","Name":"Kabutops"}]},{"Number":"141","Name":"Kabutops","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Fury Cutter","Mud Shot"],"Weight":"40.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"140","Name":"Kabuto"}]},{"Number":"142","Name":"Aerodactyl","Classification":"Fossil Pokemon","Type I":["Rock"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Ice","Rock","Steel"],"Fast Attack(s)":["Bite","Steel Wing"],"Weight":"59.0 kg","Height":"1.8 m"},{"Number":"143","Name":"Snorlax","Classification":"Sleeping Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"460.0 kg","Height":"2.1 m"},{"Number":"144","Name":"Articuno","Classification":"Freeze Pokemon","Type I":["Ice"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Rock","Steel"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"55.4 kg","Height":"1.7 m"},{"Number":"145","Name":"Zapdos","Classification":"Electric Pokemon","Type I":["Electric"],"Type II":["Flying"],"Weaknesses":["Ice","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"52.6 kg","Height":"1.6 m"},{"Number":"146","Name":"Moltres","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"60.0 kg","Height":"2.0 m"},{"Number":"147","Name":"Dratini","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"3.3 kg","Height":"1.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Dratini candies"}},{"Number":"148","Name":"Dragonair","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"16.5 kg","Height":"4.0 m","Next Evolution Requirements":{"Amount":100,"Name":"Dratini candies"},"Next evolution(s)":[{"Number":"149","Name":"Dragonite"}]},{"Number":"149","Name":"Dragonite","Classification":"Dragon Pokemon","Type I":["Dragon"],"Type II":["Flying"],"Weaknesses":["Ice","Rock","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath","Steel Wing"],"Weight":"210.0 kg","Height":"2.2 m","Previous evolution(s)":[{"Number":"148","Name":"Dragonair"}]},{"Number":"150","Name":"Mewtwo","Classification":"Genetic Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"122.0 kg","Height":"2.0 m"},{"Number":"151","Name":"Mew","Classification":"New Species Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.4 m"}] \ No newline at end of file +[{"Number":"001","Name":"Bulbasaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Tackle","Vine Whip"],"Weight":"6.9 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"002","Name":"Ivysaur"},{"Number":"003","Name":"Venusaur"}]},{"Number":"002","Name":"Ivysaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"13.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"003","Name":"Venusaur"}]},{"Number":"003","Name":"Venusaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"100.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"},{"Number":"002","Name":"Ivysaur"}]},{"Number":"004","Name":"Charmander","Classification":"Lizard Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Scratch"],"Weight":"8.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"005","Name":"Charmeleon"},{"Number":"006","Name":"Charizard"}]},{"Number":"005","Name":"Charmeleon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"19.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"}],"Next Evolution Requirements":{"Amount":100,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"006","Name":"Charizard"}]},{"Number":"006","Name":"Charizard","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Ember","Wing Attack"],"Weight":"90.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"},{"Number":"005","Name":"Charmeleon"}]},{"Number":"007","Name":"Squirtle","Classification":"Tiny Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Tackle","Bubble"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"008","Name":"Wartortle"},{"Number":"009","Name":"Blastoise"}]},{"Number":"008","Name":"Wartortle","Classification":"Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"22.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"}],"Next Evolution Requirements":{"Amount":100,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"009","Name":"Blastoise"}]},{"Number":"009","Name":"Blastoise","Classification":"Shellfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"85.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"},{"Number":"008","Name":"Wartortle"}]},{"Number":"010","Name":"Caterpie","Classification":"Worm Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"2.9 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"011","Name":"Metapod"},{"Number":"012","Name":"Butterfree"}]},{"Number":"011","Name":"Metapod","Classification":"Cocoon Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"9.9 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"}],"Next Evolution Requirements":{"Amount":50,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"012","Name":"Butterfree"}]},{"Number":"012","Name":"Butterfree","Classification":"Butterfly Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"32.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"},{"Number":"011","Name":"Metapod"}]},{"Number":"013","Name":"Weedle","Classification":"Hairy Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Sting"],"Weight":"3.2 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"014","Name":"Kakuna"},{"Number":"015","Name":"Beedrill"}]},{"Number":"014","Name":"Kakuna","Classification":"Cocoon Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Posion Sting"],"Weight":"10.0 kg","Height":"0.6 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"}],"Next Evolution Requirements":{"Amount":50,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"015","Name":"Beedrill"}]},{"Number":"015","Name":"Beedrill","Classification":"Poison Bee Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Jab"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"},{"Number":"014","Name":"Kakuna"}]},{"Number":"016","Name":"Pidgey","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"1.8 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"017","Name":"Pidgeotto"},{"Number":"018","Name":"Pidgeot"}]},{"Number":"017","Name":"Pidgeotto","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"30.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"}],"Next Evolution Requirements":{"Amount":50,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"018","Name":"Pidgeot"}]},{"Number":"018","Name":"Pidgeot","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Hurricane"],"Weight":"39.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"},{"Number":"017","Name":"Pidgeotto"}]},{"Number":"019","Name":"Rattata","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Body Slam","Dig","Hyper Fang"],"Weight":"3.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Rattata candies"},"Next evolution(s)":[{"Number":"020","Name":"Raticate"}]},{"Number":"020","Name":"Raticate","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Quick Attack"],"Special Attack(s)":["Dig","Hyper Beam","Hyper Fang"],"Weight":"18.5 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"019","Name":"Rattata"}]},{"Number":"021","Name":"Spearow","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"2.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Spearow candies"},"Next evolution(s)":[{"Number":"022","Name":"Fearow"}]},{"Number":"022","Name":"Fearow","Classification":"Beak Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Steel Wing"],"Weight":"38.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"021","Name":"Spearow"}]},{"Number":"023","Name":"Ekans","Classification":"Snake Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Sting"],"Weight":"6.9 kg","Height":"2.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ekans candies"},"Next evolution(s)":[{"Number":"024","Name":"Arbok"}]},{"Number":"024","Name":"Arbok","Classification":"Cobra Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Bite"],"Weight":"65.0 kg","Height":"3.5 m","Previous evolution(s)":[{"Number":"023","Name":"Ekans"}]},{"Number":"025","Name":"Pikachu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Quick Attack","Thunder Shock"],"Weight":"6.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Pikachu candies"},"Next evolution(s)":[{"Number":"026","Name":"Raichu"}]},{"Number":"026","Name":"Raichu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock","Spark"],"Weight":"30.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"025","Name":"Pikachu"}]},{"Number":"027","Name":"Sandshrew","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"12.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Sandshrew candies"},"Next evolution(s)":[{"Number":"028","Name":"Sandslash"}]},{"Number":"028","Name":"Sandslash","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"027","Name":"Sandshrew"}]},{"Number":"029","Name":"Nidoran F","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"7.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"030","Name":"Nidorina"},{"Number":"031","Name":"Nidoqueen"}]},{"Number":"030","Name":"Nidorina","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"20.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"031","Name":"Nidoqueen"}]},{"Number":"031","Name":"Nidoqueen","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"},{"Number":"030","Name":"Nidorina"}]},{"Number":"032","Name":"Nidoran M","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Peck","Poison Sting"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"033","Name":"Nidorino"},{"Number":"034","Name":"Nidoking"}]},{"Number":"033","Name":"Nidorino","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"19.5 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"034","Name":"Nidoking"}]},{"Number":"034","Name":"Nidoking","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Fury Cutter","Poison Jab"],"Weight":"62.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"},{"Number":"033","Name":"Nidorino"}]},{"Number":"035","Name":"Clefairy","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"7.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Clefairy candies"},"Next evolution(s)":[{"Number":"036","Name":"Clefable"}]},{"Number":"036","Name":"Clefable","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"40.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"035","Name":"Clefairy"}]},{"Number":"037","Name":"Vulpix","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"9.9 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Vulpix candies"},"Next evolution(s)":[{"Number":"038","Name":"Ninetales"}]},{"Number":"038","Name":"Ninetales","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"19.9 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"037","Name":"Vulpix"}]},{"Number":"039","Name":"Jigglypuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"5.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Jigglypuff candies"},"Next evolution(s)":[{"Number":"039","Name":"Jigglypuff"}]},{"Number":"040","Name":"Wigglytuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"12.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"040","Name":"Wigglytuff"}]},{"Number":"041","Name":"Zubat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Quick Attack"],"Weight":"7.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Zubat candies"},"Next evolution(s)":[{"Number":"042","Name":"Golbat"}]},{"Number":"042","Name":"Golbat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Wing Attack"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"041","Name":"Zubat"}]},{"Number":"043","Name":"Oddish","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"5.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"044","Name":"Gloom"},{"Number":"045","Name":"Vileplume"}]},{"Number":"044","Name":"Gloom","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"8.6 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"}],"Next Evolution Requirements":{"Amount":100,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"045","Name":"Vileplume"}]},{"Number":"045","Name":"Vileplume","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid",""],"Weight":"18.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"},{"Number":"044","Name":"Gloom"}]},{"Number":"046","Name":"Paras","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Scratch"],"Weight":"5.4 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Paras candies"},"Next evolution(s)":[{"Number":"047","Name":"Parasect"}]},{"Number":"047","Name":"Parasect","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Fury Cutter"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"046","Name":"Paras"}]},{"Number":"048","Name":"Venonat","Classification":"Insect Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Venonat candies"},"Next evolution(s)":[{"Number":"049","Name":"Venomoth"}]},{"Number":"049","Name":"Venomoth","Classification":"Poison Moth Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"12.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"048","Name":"Venonat"}]},{"Number":"050","Name":"Diglett","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"0.8 kg","Height":"0.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Diglett candies"},"Next evolution(s)":[{"Number":"051","Name":"Dugtrio"}]},{"Number":"051","Name":"Dugtrio","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Sucker Punch"],"Weight":"33.3 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"050","Name":"Diglett"}]},{"Number":"052","Name":"Meowth","Classification":"Scratch Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Scratch"],"Weight":"4.2 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Meowth candies"},"Next evolution(s)":[{"Number":"053","Name":"Persian"}]},{"Number":"053","Name":"Persian","Classification":"Classy Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Scratch"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"052","Name":"Meowth"}]},{"Number":"054","Name":"Psyduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun","Zen Headbutt"],"Weight":"19.6 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Psyduck candies"},"Next evolution(s)":[{"Number":"055","Name":"Golduck"}]},{"Number":"055","Name":"Golduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"76.6 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"054","Name":"Psyduck"}]},{"Number":"056","Name":"Mankey","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Scratch"],"Weight":"28.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Mankey candies"},"Next evolution(s)":[{"Number":"057","Name":"Primeape"}]},{"Number":"057","Name":"Primeape","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"056","Name":"Mankey"}]},{"Number":"058","Name":"Growlithe","Classification":"Puppy Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Ember"],"Weight":"19.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":50,"Name":"Growlithe candies"},"Next evolution(s)":[{"Number":"059","Name":"Arcanine"}]},{"Number":"059","Name":"Arcanine","Classification":"Legendary Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Fire Fang"],"Weight":"155.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"058","Name":"Growlithe"}]},{"Number":"060","Name":"Poliwag","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"12.4 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"061","Name":"Poliwhirl"},{"Number":"062","Name":"Poliwrath"}]},{"Number":"061","Name":"Poliwhirl","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"20.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"}],"Next Evolution Requirements":{"Amount":100,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"062","Name":"Poliwrath"}]},{"Number":"062","Name":"Poliwrath","Classification":"Tadpole Pokemon","Type I":["Water"],"Type II":["Fighting"],"Weaknesses":["Electric","Grass","Flying","Psychic","Fairy"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"54.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"},{"Number":"061","Name":"Poliwhirl"}]},{"Number":"063","Name":"Abra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Zen Headbutt",""],"Weight":"19.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":25,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"064","Name":"Kadabra"},{"Number":"065","Name":"Alakazam"}]},{"Number":"064","Name":"Kadabra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"56.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"}],"Next Evolution Requirements":{"Amount":100,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"065","Name":"Alakazam"}]},{"Number":"065","Name":"Alakazam","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"48.0 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"},{"Number":"064","Name":"Kadabra"}]},{"Number":"066","Name":"Machop","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"19.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"067","Name":"Machoke"},{"Number":"068","Name":"Machamp"}]},{"Number":"067","Name":"Machoke","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"70.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"}],"Next Evolution Requirements":{"Amount":100,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"068","Name":"Machamp"}]},{"Number":"068","Name":"Machamp","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Karate Chop"],"Weight":"130.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"},{"Number":"067","Name":"Machoke"}]},{"Number":"069","Name":"Bellsprout","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Vine Whip"],"Weight":"4.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"070","Name":"Weepinbell"},{"Number":"071","Name":"Victreebel"}]},{"Number":"070","Name":"Weepinbell","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"6.4 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"071","Name":"Victreebel"}]},{"Number":"071","Name":"Victreebel","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"15.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"},{"Number":"070","Name":"Weepinbell"}]},{"Number":"072","Name":"Tentacool","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Bubble","Poison Sting"],"Weight":"45.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Tentacool candies"},"Next evolution(s)":[{"Number":"073","Name":"Tentacruel"}]},{"Number":"073","Name":"Tentacruel","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Jab"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"072","Name":"Tentacool"}]},{"Number":"074","Name":"Geodude","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"20.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"075","Name":"Graveler"},{"Number":"076","Name":"Golem"}]},{"Number":"075","Name":"Graveler","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"105.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"}],"Next Evolution Requirements":{"Amount":100,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"076","Name":"Golem"}]},{"Number":"076","Name":"Golem","Classification":"Megaton Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"300.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"},{"Number":"075","Name":"Graveler"}]},{"Number":"077","Name":"Ponyta","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Tackle"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ponyta candies"},"Next evolution(s)":[{"Number":"078","Name":"Rapidash"}]},{"Number":"078","Name":"Rapidash","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Low Kick"],"Weight":"95.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"077","Name":"Ponyta"}]},{"Number":"079","Name":"Slowpoke","Classification":"Dopey Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"36.0 kg","Height":"1.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Slowpoke candies"},"Next evolution(s)":[{"Number":"080","Name":"Slowbro"}]},{"Number":"080","Name":"Slowbro","Classification":"Hermit Crab Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"78.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"079","Name":"Slowpoke"}]},{"Number":"081","Name":"Magnemite","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"6.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Magnemite candies"},"Next evolution(s)":[{"Number":"082","Name":"Magneton"}]},{"Number":"082","Name":"Magneton","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"60.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"081","Name":"Magnemite"}]},{"Number":"083","Name":"Farfetch'd","Classification":"Wild Duck Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"15.0 kg","Height":"0.8 m"},{"Number":"084","Name":"Doduo","Classification":"Twin Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"39.2 kg","Height":"1.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Doduo candies"},"Next evolution(s)":[{"Number":"085","Name":"Dodrio"}]},{"Number":"085","Name":"Dodrio","Classification":"Triple Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Feint Attack","Steel Wing"],"Weight":"85.2 kg","Height":"1.8 m","Previous evolution(s)":[{"Number":"084","Name":"Doduo"}]},{"Number":"086","Name":"Seel","Classification":"Sea Lion Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Water Gun"],"Weight":"90.0 kg","Height":"1.1 m","Next Evolution Requirements":{"Amount":50,"Name":"Seel candies"},"Next evolution(s)":[{"Number":"087","Name":"Dewgong"}]},{"Number":"087","Name":"Dewgong","Classification":"Sea Lion Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"120.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"086","Name":"Seel"}]},{"Number":"088","Name":"Grimer","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Mud Slap"],"Weight":"30.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Grimer candies"},"Next evolution(s)":[{"Number":"089","Name":"Muk"}]},{"Number":"089","Name":"Muk","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Poison Jab",""],"Weight":"30.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"088","Name":"Grimer"}]},{"Number":"090","Name":"Shellder","Classification":"Bivalve Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Tackle"],"Weight":"4.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Shellder candies"},"Next evolution(s)":[{"Number":"091","Name":"Cloyster"}]},{"Number":"091","Name":"Cloyster","Classification":"Bivalve Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"132.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"090","Name":"Shellder"}]},{"Number":"092","Name":"Gastly","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Sucker Punch"],"Weight":"0.1 kg","Height":"1.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"093","Name":"Haunter"},{"Number":"094","Name":"Gengar"}]},{"Number":"093","Name":"Haunter","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Shadow Claw"],"Weight":"0.1 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"}],"Next Evolution Requirements":{"Amount":100,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"094","Name":"Gengar"}]},{"Number":"094","Name":"Gengar","Classification":"Shadow Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Shadow Claw","Sucker Punch"],"Weight":"40.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"},{"Number":"093","Name":"Haunter"}]},{"Number":"095","Name":"Onix","Classification":"Rock Snake Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"210.0 kg","Height":"8.8 m"},{"Number":"096","Name":"Drowzee","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Pound"],"Weight":"32.4 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Drowzee candies"},"Next evolution(s)":[{"Number":"097","Name":"Hypno"}]},{"Number":"097","Name":"Hypno","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"75.6 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"096","Name":"Drowzee"}]},{"Number":"098","Name":"Krabby","Classification":"River Crab Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Krabby candies"},"Next evolution(s)":[{"Number":"099","Name":"Kingler"}]},{"Number":"099","Name":"Kingler","Classification":"Pincer Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"098","Name":"Krabby"}]},{"Number":"100","Name":"Voltorb","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark","Tackle"],"Weight":"10.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Voltorb candies"},"Next evolution(s)":[{"Number":"101","Name":"Electrode"}]},{"Number":"101","Name":"Electrode","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark",""],"Weight":"66.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"100","Name":"Voltorb"}]},{"Number":"102","Name":"Exeggcute","Classification":"Egg Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion",""],"Weight":"2.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Exeggcute candies"},"Next evolution(s)":[{"Number":"103","Name":"Exeggutor"}]},{"Number":"103","Name":"Exeggutor","Classification":"Coconut Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"120.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"102","Name":"Exeggcute"}]},{"Number":"104","Name":"Cubone","Classification":"Lonely Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Cubone candies"},"Next evolution(s)":[{"Number":"105","Name":"Marowak"}]},{"Number":"105","Name":"Marowak","Classification":"Bone Keeper Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"45.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"104","Name":"Cubone"}]},{"Number":"106","Name":"Hitmonlee","Classification":"Kicking Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Low Kick","Rock Smash"],"Weight":"49.8 kg","Height":"1.5 m","Next evolution(s)":[{"Number":"107","Name":"Hitmonchan"}]},{"Number":"107","Name":"Hitmonchan","Classification":"Punching Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Rock Smash"],"Weight":"50.2 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"106","Name":"Hitmonlee"}]},{"Number":"108","Name":"Lickitung","Classification":"Licking Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"65.5 kg","Height":"1.2 m"},{"Number":"109","Name":"Koffing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"1.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Koffing candies"},"Next evolution(s)":[{"Number":"110","Name":"Weezing"}]},{"Number":"110","Name":"Weezing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"9.5 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"109","Name":"Koffing"}]},{"Number":"111","Name":"Rhyhorn","Classification":"Spikes Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"115.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Rhyhorn candies"},"Next evolution(s)":[{"Number":"112","Name":"Rhydon"}]},{"Number":"112","Name":"Rhydon","Classification":"Drill Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"120.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"111","Name":"Rhyhorn"}]},{"Number":"113","Name":"Chansey","Classification":"Egg Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"34.6 kg","Height":"1.1 m"},{"Number":"114","Name":"Tangela","Classification":"Vine Pokemon","Type I":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug"],"Fast Attack(s)":["Vine Whip",""],"Weight":"35.0 kg","Height":"1.0 m"},{"Number":"115","Name":"Kangaskhan","Classification":"Parent Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Low Kick",""],"Weight":"80.0 kg","Height":"2.2 m"},{"Number":"116","Name":"Horsea","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Water Gun"],"Weight":"8.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Horsea candies"},"Next evolution(s)":[{"Number":"117","Name":"Seadra"}]},{"Number":"117","Name":"Seadra","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Dragon Breath","Water Gun"],"Weight":"25.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"116","Name":"Horsea"}]},{"Number":"118","Name":"Goldeen","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Mud Shot"],"Weight":"15.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Goldeen candies"},"Next evolution(s)":[{"Number":"119","Name":"Seaking"}]},{"Number":"119","Name":"Seaking","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Poison Jab"],"Weight":"39.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"118","Name":"Goldeen"}]},{"Number":"120","Name":"Staryu","Classification":"Starshape Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"34.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Staryu candies"},"Next evolution(s)":[{"Number":"120","Name":"Staryu"}]},{"Number":"121","Name":"Starmie","Classification":"Mysterious Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"80.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"121","Name":"Starmie"}]},{"Number":"122","Name":"Mr. Mime","Classification":"Barrier Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"54.5 kg","Height":"1.3 m"},{"Number":"123","Name":"Scyther","Classification":"Mantis Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Steel Wing"],"Weight":"56.0 kg","Height":"1.5 m"},{"Number":"124","Name":"Jynx","Classification":"Humanshape Pokemon","Type I":["Ice"],"Type II":["Psychic"],"Weaknesses":["Fire","Bug","Rock","Ghost","Dark","Steel"],"Fast Attack(s)":["Frost Breath","Pound"],"Weight":"40.6 kg","Height":"1.4 m"},{"Number":"125","Name":"Electabuzz","Classification":"Electric Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Low Kick","Thunder Shock"],"Weight":"30.0 kg","Height":"1.1 m"},{"Number":"126","Name":"Magmar","Classification":"Spitfire Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Karate Chop"],"Weight":"44.5 kg","Height":"1.3 m"},{"Number":"127","Name":"Pinsir","Classification":"Stagbeetle Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Rock Smash"],"Weight":"55.0 kg","Height":"1.5 m"},{"Number":"128","Name":"Tauros","Classification":"Wild Bull Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Tackle","Zen Headbutt"],"Weight":"88.4 kg","Height":"1.4 m"},{"Number":"129","Name":"Magikarp","Classification":"Fish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Splash",""],"Weight":"10.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":400,"Name":"Magikarp candies"},"Next evolution(s)":[{"Number":"130","Name":"Gyarados"}]},{"Number":"130","Name":"Gyarados","Classification":"Atrocious Pokemon","Type I":["Water"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Bite","Dragon Breath"],"Weight":"235.0 kg","Height":"6.5 m","Previous evolution(s)":[{"Number":"129","Name":"Magikarp"}]},{"Number":"131","Name":"Lapras","Classification":"Transport Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"220.0 kg","Height":"2.5 m"},{"Number":"132","Name":"Ditto","Classification":"Transform Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.3 m"},{"Number":"133","Name":"Eevee","Classification":"Evolution Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"6.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Eevee candies"},"Next evolution(s)":[{"Number":"134","Name":"Vaporeon"},{"Number":"135","Name":"Jolteon"},{"Number":"136","Name":"Flareon"}]},{"Number":"134","Name":"Vaporeon","Classification":"Bubble Jet Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun",""],"Weight":"29.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"135","Name":"Jolteon","Classification":"Lightning Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock",""],"Weight":"24.5 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"136","Name":"Flareon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"25.0 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"137","Name":"Porygon","Classification":"Virtual Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"36.5 kg","Height":"0.8 m"},{"Number":"138","Name":"Omanyte","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Water Gun",""],"Weight":"7.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Omanyte candies"},"Next evolution(s)":[{"Number":"139","Name":"Omastar"}]},{"Number":"139","Name":"Omastar","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Rock Throw","Water Gun"],"Weight":"35.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"138","Name":"Omanyte"}]},{"Number":"140","Name":"Kabuto","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"11.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Kabuto candies"},"Next evolution(s)":[{"Number":"141","Name":"Kabutops"}]},{"Number":"141","Name":"Kabutops","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Fury Cutter","Mud Shot"],"Weight":"40.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"140","Name":"Kabuto"}]},{"Number":"142","Name":"Aerodactyl","Classification":"Fossil Pokemon","Type I":["Rock"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Ice","Rock","Steel"],"Fast Attack(s)":["Bite","Steel Wing"],"Weight":"59.0 kg","Height":"1.8 m"},{"Number":"143","Name":"Snorlax","Classification":"Sleeping Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"460.0 kg","Height":"2.1 m"},{"Number":"144","Name":"Articuno","Classification":"Freeze Pokemon","Type I":["Ice"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Rock","Steel"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"55.4 kg","Height":"1.7 m"},{"Number":"145","Name":"Zapdos","Classification":"Electric Pokemon","Type I":["Electric"],"Type II":["Flying"],"Weaknesses":["Ice","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"52.6 kg","Height":"1.6 m"},{"Number":"146","Name":"Moltres","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"60.0 kg","Height":"2.0 m"},{"Number":"147","Name":"Dratini","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"3.3 kg","Height":"1.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Dratini candies"}},{"Number":"148","Name":"Dragonair","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"16.5 kg","Height":"4.0 m","Previous evolution(s)":[{"Number":"147","Name":"Dratini"}],"Next Evolution Requirements":{"Amount":100,"Name":"Dratini candies"},"Next evolution(s)":[{"Number":"149","Name":"Dragonite"}]},{"Number":"149","Name":"Dragonite","Classification":"Dragon Pokemon","Type I":["Dragon"],"Type II":["Flying"],"Weaknesses":["Ice","Rock","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath","Steel Wing"],"Weight":"210.0 kg","Height":"2.2 m","Previous evolution(s)":[{"Number":"148","Name":"Dragonair"}]},{"Number":"150","Name":"Mewtwo","Classification":"Genetic Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"122.0 kg","Height":"2.0 m"},{"Number":"151","Name":"Mew","Classification":"New Species Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.4 m"}] diff --git a/pokemongo_bot/cell_workers/evolve_all.py b/pokemongo_bot/cell_workers/evolve_all.py deleted file mode 100644 index b23624ad92..0000000000 --- a/pokemongo_bot/cell_workers/evolve_all.py +++ /dev/null @@ -1,349 +0,0 @@ -from pokemongo_bot import logger -from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot.item_list import Item -from pokemongo_bot.cell_workers.base_task import BaseTask - -class EvolveAll(BaseTask): - def initialize(self): - self.evolve_all = self.config.get('evolve_all', []) - self.evolve_speed = self.config.get('evolve_speed', 3.7) - self.evolve_cp_min = self.config.get('evolve_cp_min', 300) - self.evolve_num_min = self.config.get('evolve_num_min', 5) - self.use_lucky_egg = self.config.get('use_lucky_egg', False) - self._validate_config() - - def _validate_config(self): - if isinstance(self.evolve_all, basestring): - self.evolve_all = [str(pokemon_name) for pokemon_name in self.evolve_all.split(',')] - - def work(self): - if not self._should_run(): - return - - response_dict = self.bot.get_inventory() - cache = {} - - try: - reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) - except KeyError: - pass - else: - inventory_items = response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] - candy_data = self._get_candy_data(inventory_items) - evolve_list = self._sort_by_cp_iv(inventory_items) - - # filter out non-listed pokemen and those with not enough candy - evolve_list = [x for x in evolve_list if self._is_evolvable(x, candy_data)] - - # Don't evolve unless the evolvable candidates number is no less than evolve_num_min - if len(evolve_list) < self.evolve_num_min: - # logger.log('Evolvable candidates number is {}, which is less than {}... skipping evolve.'.format( - # len(evolve_list), self.evolve_num_min), - # 'green') - return - - if self.use_lucky_egg: - lucky_egg_count = self.bot.item_inventory_count(Item.ITEM_LUCKY_EGG.value) - # Sometimes remaining lucky egg count get changed, check again for sure - if lucky_egg_count <= 0: - logger.log('No lucky eggs... skipping evolve!', 'yellow') - return - - logger.log('Using lucky egg ... you have {}'.format(lucky_egg_count)) - response_dict_lucky_egg = self.bot.use_lucky_egg() - if response_dict_lucky_egg and 'responses' in response_dict_lucky_egg and \ - 'USE_ITEM_XP_BOOST' in response_dict_lucky_egg['responses'] and \ - 'result' in response_dict_lucky_egg['responses']['USE_ITEM_XP_BOOST']: - result = response_dict_lucky_egg['responses']['USE_ITEM_XP_BOOST']['result'] - if result is 1: # Request success - logger.log('Successfully used lucky egg... ({} left!)'.format(lucky_egg_count - 1), 'green') - else: - logger.log('Failed to use lucky egg!', 'red') - return - - # enable to limit number of pokemons to evolve. Useful for testing. - # nn = 3 - # if len(evolve_list) > nn: - # evolve_list = evolve_list[:nn] - # - - id_list1 = self.count_pokemon_inventory() - for pokemon in evolve_list: - try: - self._execute_pokemon_evolve(pokemon, cache) - except Exception: - pass - id_list2 = self.count_pokemon_inventory() - release_cand_list_ids = list(set(id_list2) - set(id_list1)) - - if release_cand_list_ids: - logger.log('[#] Evolved {} pokemons! Checking if any of them needs to be released ...'.format( - len(release_cand_list_ids) - )) - self._release_evolved(release_cand_list_ids) - - def _should_run(self): - # Don't run after the first tick - # Lucky Egg should only be popped at the first tick - if not self.evolve_all or self.bot.tick_count is 1: - return False - - # Will skip evolving if user wants to use an egg and there is none - lucky_egg_count = self.bot.item_inventory_count(Item.ITEM_LUCKY_EGG.value) - if self.use_lucky_egg and lucky_egg_count <= 0: - logger.log('No lucky eggs... skipping evolve!', 'yellow') - return False - - # Otherwise try evolving - return True - - - def _release_evolved(self, release_cand_list_ids): - response_dict = self.bot.get_inventory() - cache = {} - - try: - reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) - except KeyError: - pass - else: - release_cand_list = self._sort_by_cp_iv( - response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']) - release_cand_list = [x for x in release_cand_list if x[0] in release_cand_list_ids] - - ## at this point release_cand_list contains evolved pokemons data - for cand in release_cand_list: - pokemon_id = cand[0] - pokemon_name = cand[1] - pokemon_cp = cand[2] - pokemon_potential = cand[3] - - if self.should_release_pokemon(pokemon_name, pokemon_cp, pokemon_potential): - # Transfering Pokemon - self.transfer_pokemon(pokemon_id) - logger.log( - '[#] {} has been exchanged for candy!'.format(pokemon_name), 'red') - - def _get_candy_data(self, inventory_items): - candy = {} - for item in inventory_items: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "candy"], item) - except KeyError: - pass - else: - try: - pokemon_candy = item['inventory_item_data']['candy'] - candy[pokemon_candy['family_id']] = pokemon_candy['candy'] - except Exception: - pass - return candy - - - def _sort_by_cp_iv(self, inventory_items): - pokemons1 = [] - pokemons2 = [] - for item in inventory_items: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data"], item) - except KeyError: - pass - else: - try: - pokemon = item['inventory_item_data']['pokemon_data'] - pokemon_num = int(pokemon['pokemon_id']) - 1 - pokemon_name = self.bot.pokemon_list[int(pokemon_num)]['Name'] - v = [ - pokemon['id'], - pokemon_name, - pokemon['cp'], - self._compute_iv(pokemon), - pokemon['pokemon_id'] - ] - if pokemon['cp'] > self.evolve_cp_min: - pokemons1.append(v) - else: - pokemons2.append(v) - except Exception: - pass - - # Sort larger CP pokemons by IV, tie breaking by CP - pokemons1.sort(key=lambda x: (x[3], x[2]), reverse=True) - - # Sort smaller CP pokemons by CP, tie breaking by IV - pokemons2.sort(key=lambda x: (x[2], x[3]), reverse=True) - - return pokemons1 + pokemons2 - - def _is_evolvable(self, pokemon, candy_data): - pokemon_name = pokemon[1] - family_id = pokemon[4] - # python list is index 0 based, thus - 1 - pokemon_idx = int(family_id) - 1 - - # Non-evolvable or top-tier pokemon - if 'Next Evolution Requirements' not in self.bot.pokemon_list[pokemon_idx]: - return False - - # filter out non-listed pokemen - if self.evolve_all[0] != 'all' and pokemon_name not in self.evolve_all: - return False - - # filter out those with not enough candy - if 'Previous evolution(s)' in self.bot.pokemon_list[pokemon_idx]: - family_id = int(self.bot.pokemon_list[pokemon_idx]['Previous evolution(s)'][0]['Number']) - - need_candies = int(self.bot.pokemon_list[pokemon_idx]['Next Evolution Requirements']['Amount']) - # print('{} need {} candies to evolve, currently have {}'. - # format(pokemon_name, need_candies, candy_data[family_id])) - if candy_data[family_id] >= need_candies: - candy_data[family_id] -= need_candies - return True - return False - - def _execute_pokemon_evolve(self, pokemon, cache): - pokemon_id = pokemon[0] - pokemon_name = pokemon[1] - pokemon_cp = pokemon[2] - pokemon_iv = pokemon[3] - - if pokemon_name in cache: - return - - self.bot.api.evolve_pokemon(pokemon_id=pokemon_id) - response_dict = self.bot.api.call() - status = response_dict['responses']['EVOLVE_POKEMON']['result'] - if status == 1: - logger.log('[#] Successfully evolved {} with {} CP and {} IV!'.format( - pokemon_name, pokemon_cp, pokemon_iv - )) - - sleep(self.evolve_speed) - - else: - # cache pokemons we can't evolve. Less server calls - cache[pokemon_name] = 1 - sleep(0.7) - - # 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() - - def count_pokemon_inventory(self): - response_dict = self.bot.get_inventory() - id_list = [] - return self.counting_pokemon(response_dict, id_list) - - def counting_pokemon(self, response_dict, id_list): - try: - reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) - except KeyError: - pass - else: - for item in response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data"], item) - except KeyError: - pass - else: - pokemon = item['inventory_item_data']['pokemon_data'] - if pokemon.get('is_egg', False): - continue - id_list.append(pokemon['id']) - - return id_list - - def should_release_pokemon(self, pokemon_name, cp, iv): - if self._check_always_capture_exception_for(pokemon_name): - return False - else: - release_config = self._get_release_config_for(pokemon_name) - cp_iv_logic = release_config.get('logic') - if not cp_iv_logic: - cp_iv_logic = self._get_release_config_for('any').get('logic', 'and') - - release_results = { - 'cp': False, - 'iv': False, - } - - if 'release_below_cp' in release_config: - min_cp = release_config['release_below_cp'] - if cp < min_cp: - release_results['cp'] = True - - if 'release_below_iv' in release_config: - min_iv = release_config['release_below_iv'] - if iv < min_iv: - release_results['iv'] = True - - if release_config.get('always_release'): - return True - - logic_to_function = { - 'or': lambda x, y: x or y, - 'and': lambda x, y: x and y - } - - # logger.log( - # "[x] Release config for {}: CP {} {} IV {}".format( - # pokemon_name, - # min_cp, - # cp_iv_logic, - # min_iv - # ), 'yellow' - # ) - - return logic_to_function[cp_iv_logic](*release_results.values()) - - def _get_release_config_for(self, pokemon): - release_config = self.bot.config.release.get(pokemon) - if not release_config: - release_config = self.bot.config.release['any'] - return release_config - - def _get_exceptions(self): - exceptions = self.bot.config.release.get('exceptions') - if not exceptions: - return None - return exceptions - - def _get_always_capture_list(self): - exceptions = self._get_exceptions() - if not exceptions: - return [] - always_capture_list = exceptions['always_capture'] - if not always_capture_list: - return [] - return always_capture_list - - def _check_always_capture_exception_for(self, pokemon_name): - always_capture_list = self._get_always_capture_list() - if not always_capture_list: - return False - else: - for pokemon in always_capture_list: - if pokemon_name == str(pokemon): - return True - return False - - # TODO: should also go to util and refactor in catch worker - def _compute_iv(self, pokemon): - total_IV = 0.0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - - for individual_stat in iv_stats: - try: - total_IV += pokemon[individual_stat] - except Exception: - pokemon[individual_stat] = 0 - continue - pokemon_potential = round((total_IV / 45.0), 2) - return pokemon_potential diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 52013ebbcc..01d1b6e6f0 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -11,6 +11,7 @@ def initialize(self): self.first_evolve_by = self.config.get('first_evolve_by', 'cp') self.evolve_above_cp = self.config.get('evolve_above_cp', 500) self.evolve_above_iv = self.config.get('evolve_above_iv', 0.8) + self.evolve_num_min = self.config.get('evolve_num_min', 5) self.cp_iv_logic = self.config.get('logic', 'or') self.use_lucky_egg = self.config.get('use_lucky_egg', False) self._validate_config() @@ -24,63 +25,79 @@ def work(self): 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) - - if self.evolve_all[0] != 'all': - # filter out non-listed pokemons - evolve_list = filter(lambda x: x["name"] in self.evolve_all, evolve_list) - - cache = {} - candy_list = self._get_candy_list(inventory_items) - evolved = 0 - for pokemon in evolve_list: - if self._can_evolve(pokemon, candy_list, cache) \ - and self._execute_pokemon_evolve(pokemon, candy_list, cache): - evolved += 1 - - if evolved > 0: - logger.log('Evolved {} pokemon!'.format(evolved)) + cache = set() + try: + reduce(dict.__getitem__, [ + "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) + except KeyError: + pass + else: + inventory_items = response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] + candy_data = self._get_candy_data(inventory_items) + evolve_list = self._sort_and_filter(inventory_items) + + # filter out non-listed pokemons, top-tier pokemons and those with not enough candy + evolve_list = [x for x in evolve_list if self._is_evolvable(x, candy_data)] + + # Don't evolve unless the evolvable candidates number is no less than evolve_num_min + if len(evolve_list) < self.evolve_num_min: + # logger.log('Evolvable candidates number is {}, which is less than {}... skipping evolve.'.format( + # len(evolve_list), self.evolve_num_min), + # 'green') + return + + if self.use_lucky_egg: + lucky_egg_count = self.bot.item_inventory_count(Item.ITEM_LUCKY_EGG.value) + # Sometimes remaining lucky egg count get changed, check again for sure + if lucky_egg_count <= 0: + logger.log('No lucky eggs... skipping evolve!', 'yellow') + return + + logger.log('Using lucky egg ... you have {}'.format(lucky_egg_count)) + response_dict_lucky_egg = self.bot.use_lucky_egg() + if response_dict_lucky_egg and 'responses' in response_dict_lucky_egg and \ + 'USE_ITEM_XP_BOOST' in response_dict_lucky_egg['responses'] and \ + 'result' in response_dict_lucky_egg['responses']['USE_ITEM_XP_BOOST']: + result = response_dict_lucky_egg['responses']['USE_ITEM_XP_BOOST']['result'] + if result is 1: # Request success + logger.log('Successfully used lucky egg... ({} left!)'.format(lucky_egg_count - 1), 'green') + else: + logger.log('Failed to use lucky egg!', 'red') + return + + evolved = 0 + for pokemon in evolve_list: + try: + if self._execute_pokemon_evolve(pokemon, cache): + evolved += 1 + except Exception: + pass + if evolved > 0: + logger.log('Evolved {} pokemon!'.format(evolved)) def _should_run(self): - if not self.evolve_all or self.evolve_all[0] == 'none': + # Don't run after the first tick + # Lucky Egg should only be popped at the first tick + if not self.evolve_all or self.evolve_all[0] == 'none' or self.bot.tick_count is 1: return False - # Evolve all is used - Use Lucky egg only at the first tick - if self.bot.tick_count is not 1 or not self.use_lucky_egg: - return True - + # Will skip evolving if user wants to use an egg and there is none lucky_egg_count = self.bot.item_inventory_count(Item.ITEM_LUCKY_EGG.value) - - # Make sure the user has a lucky egg and skip if not - if lucky_egg_count > 0: - logger.log('Using lucky egg ... you have {}'.format(lucky_egg_count)) - response_dict_lucky_egg = self.bot.use_lucky_egg() - if response_dict_lucky_egg: - result = response_dict_lucky_egg.get('responses', {}).get('USE_ITEM_XP_BOOST', {}).get('result', 0) - if result is 1: # Request success - logger.log('Successfully used lucky egg... ({} left!)'.format(lucky_egg_count - 1), 'green') - return True - else: - logger.log('Failed to use lucky egg!', 'red') - return False - else: - # Skipping evolve so they aren't wasted + if self.use_lucky_egg and lucky_egg_count <= 0: logger.log('No lucky eggs... skipping evolve!', 'yellow') return False - def _get_candy_list(self, inventory_items): + # Otherwise try evolving + return True + + def _get_candy_data(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 - + try: + candy = item['inventory_item_data']['candy'] + candies[candy['family_id']] = candy['candy'] + except KeyError: + pass return candies def _sort_and_filter(self, inventory_items): @@ -90,59 +107,85 @@ def _sort_and_filter(self, inventory_items): '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)): - pokemons.append(pokemon) + try: + reduce(dict.__getitem__, [ + "inventory_item_data", "pokemon_data"], item) + except KeyError: + pass + else: + try: + pokemon = item['inventory_item_data']['pokemon_data'] + pokemon_num = int(pokemon['pokemon_id']) - 1 + pokemon_name = self.bot.pokemon_list[int(pokemon_num)]['Name'] + pokemon = { + 'id': pokemon['id'], + 'name': pokemon_name, + 'cp': pokemon.get('cp', 0), + 'iv': self._compute_iv(pokemon), + 'pokemon_id': pokemon['pokemon_id'], + } + if logic_to_function[self.cp_iv_logic](pokemon): + pokemons.append(pokemon) + except Exception: + pass 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["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["iv"], x["cp"]), reverse=True) return pokemons - def _can_evolve(self, pokemon, candy_list, cache): + def _is_evolvable(self, pokemon, candy_data): + pokemon_name = pokemon['name'] + # python list is index 0 based, thus - 1 + pokemon_idx = int(pokemon['pokemon_id']) - 1 + # Non-evolvable or top-tier pokemon + if 'Next Evolution Requirements' not in self.bot.pokemon_list[pokemon_idx]: + return False - if pokemon["name"] in cache: + # filter out non-listed pokemen + if self.evolve_all[0] != 'all' and pokemon_name not in self.evolve_all: return False - family = pokemon["candies_family"] - amount = pokemon["candies_amount"] - if family in candy_list and candy_list[family] >= amount: + # filter out those with not enough candy + family_id = pokemon['pokemon_id'] + if 'Previous evolution(s)' in self.bot.pokemon_list[pokemon_idx]: + family_id = int(self.bot.pokemon_list[pokemon_idx]['Previous evolution(s)'][0]['Number']) + + need_candies = int(self.bot.pokemon_list[pokemon_idx]['Next Evolution Requirements']['Amount']) + # print('{} needs {} {} candies to evolve, currently we have {}'. + # format(pokemon_name, need_candies, + # self.bot.pokemon_list[int(family_id) - 1]['Name'], + # candy_data[family_id])) + if candy_data[family_id] >= need_candies: + candy_data[family_id] -= need_candies return True - else: - cache[pokemon["name"]] = 1 - return False + 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"] + + def _execute_pokemon_evolve(self, pokemon, cache): + pokemon_id = pokemon['id'] + pokemon_name = pokemon['name'] + pokemon_cp = pokemon['cp'] + pokemon_iv = pokemon['iv'] if pokemon_name in cache: return False - response_dict = self.api.evolve_pokemon(pokemon_id=pokemon_id) - if response_dict.get('responses', {}).get('EVOLVE_POKEMON', {}).get('result', 0) == 1: - logger.log('Successfully evolved {} with {} CP and {} IV!'.format(pokemon_name, pokemon_cp, pokemon_iv)) - candy_list[pokemon["candies_family"]] -= pokemon["candies_amount"] + self.bot.api.evolve_pokemon(pokemon_id=pokemon_id) + response_dict = self.bot.api.call() + status = response_dict.get('responses', {}).get('EVOLVE_POKEMON', {}).get('result', 0) + if status == 1: + logger.log('[#] Successfully evolved {} with {} CP and {} IV!'.format( + pokemon_name, pokemon_cp, pokemon_iv + )) sleep(self.evolve_speed) return True + else: # cache pokemons we can't evolve. Less server calls - cache[pokemon_name] = 1 + cache.add(pokemon_name) sleep(0.7) return False From 361c0fa11783085ea201a71a99f6c43859e8c239 Mon Sep 17 00:00:00 2001 From: Eli White Date: Tue, 2 Aug 2016 22:42:13 -0700 Subject: [PATCH 016/202] Adding a section on analytics and metrics to the Readme (#2434) --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 6fa12d4f8a..281c4d2fc8 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,15 @@ We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!]( ## Gym Battles This bot takes a strong stance against automating gym battles. Botting gyms will have a negative effect on most players and thus the game as a whole. We will thus never accept contributions or changes containing code specific for gym battles. +## Analytics +This bot is very popular and has a vibrant community. Because of that, it has become very difficult for us to know how the bot is used and what errors people hit. By capturing small amounts of data, we can prioritize our work better such as fixing errors that happen to a large percentage of our user base, not just a vocal minority. + +Our goal is to help inform our decisions by capturing data that helps us get aggregate usage and error reports, not personal information. To view the code that handles analytics in our master branch, you can use this [search link](https://github.com/PokemonGoF/PokemonGo-Bot/search?utf8=%E2%9C%93&q=BotEvent). + +If there are any concerns with this policy or you believe we are tracking something we shouldn't, please open a ticket in the tracker. The contributors always intend to do the right thing for our users, and we want to make sure we are held to that path. + +If you do not want any data to be gathered, you can turn off this feature by setting `health_record` to `false` in your `config.json`. + ## Wiki All information on [Getting Started](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Getting-Started) is available in the [Wiki](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/)! - __Installation__ From 2f91fd0410eeda430aeb9cb956885eda8bf04e22 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Wed, 3 Aug 2016 09:21:56 -0500 Subject: [PATCH 017/202] Fix to display stats on iterm2 terminal (#2440) --- CONTRIBUTORS.md | 1 + pokemongo_bot/cell_workers/update_title_stats.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index bd7894f0c2..dfd8a24af2 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -50,3 +50,4 @@ * z4ppy.bbc * matheussampaio * Abraxas000 + * lucasfevi diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index d255834870..911d2efd4d 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -6,7 +6,6 @@ from pokemongo_bot.worker_result import WorkerResult from pokemongo_bot.tree_config_builder import ConfigException - class UpdateTitleStats(BaseTask): """ Periodically updates the terminal title to display stats about the bot. @@ -108,8 +107,10 @@ def _update_title(self, title, platform): :raise: RuntimeError: When the given platform isn't supported. """ if platform == "linux" or platform == "linux2"\ - or platform == "darwin" or platform == "cygwin": + or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) + elif platform == "darwin": + stdout.write("\033]0;{}\007".format(title)) elif platform == "win32": ctypes.windll.kernel32.SetConsoleTitleA(title) else: From 02d910243d5cfa4d2a647e6b1d1fc3cf90ba7173 Mon Sep 17 00:00:00 2001 From: Benjamin G Date: Wed, 3 Aug 2016 22:45:03 +0200 Subject: [PATCH 018/202] Fix #2442 - should_retry_throttle isn't defined (#2461) * Fix #2442 Variables weren't correctly defined * Fix typo --- pokemongo_bot/api_wrapper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pokemongo_bot/api_wrapper.py b/pokemongo_bot/api_wrapper.py index a3f70aed1f..17612aceb6 100644 --- a/pokemongo_bot/api_wrapper.py +++ b/pokemongo_bot/api_wrapper.py @@ -100,10 +100,10 @@ def call(self, max_retry=15): request_timestamp = self.throttle_sleep() # self._call internally clear this field, so save it self._req_method_list = [req_method for req_method in api_req_method_list] + should_throttle_retry = False + should_unexpected_response_retry = False try: result = self._call() - should_throttle_retry = False - should_unexpected_response_retry = False except ServerSideRequestThrottlingException: should_throttle_retry = True except UnexpectedResponseException: @@ -117,7 +117,7 @@ def call(self, max_retry=15): continue # skip response checking if should_unexpected_response_retry: - unexpected_reponse_retry += 1 + unexpected_response_retry += 1 if unexpected_response_retry >= 5: logger.log('Server is not responding correctly to our requests. Waiting for 30 seconds to reconnect.', 'red') sleep(30) From b2983f7100fb6f1b4c9db833bc09a7820fea634a Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Wed, 3 Aug 2016 22:54:44 +0200 Subject: [PATCH 019/202] Replace all `logger.log` calls with events! (#2173) * bye bye `logger.log`, hello event system! * fixing travis build * trying to fix travis build * test fixes * updating remaining `logger.log` calls that should be replaced * typo * typos in IncubateEggs event * improved fort loot event data * fixing update_location event's distance unit * fixing some events and log stuff * adding missing fort_name parameter to lured_pokemon_found event * fixing a variable inside an event formatted string * fixing typos and utf8 * trying to fix tests with regards to float precision * adding command to print all registered events and their parameters * fixing tests yet again * trying to fix unicode issues, arrgh!!! * added a move to lured fort event * better distance text in move to fort and fixing utf8 in spin fort task * removing print from websocket server * start embedded server before creating the socketio_handler * I hate unicode * rename and sleep events * refactoring in how we emit events to avoid code repetition * PokemonCatch task inherits from BaseTask * go away, dirty logger.log! * pep8 and removed logging handler name attribute * good bye for the remaining logger.log calls * bye logger module * no more logger imports! * removed last few loggers * removing secret file and fixed variable name in follow cluster * fixing kwargs for event emit * trying to fix unicode handling one more time * now it works! * fixing more logs and removing debug unicode string * no logs on websocket server yet * adding a script to start a standalone websocket server * more adjusted in websocket to support multiuser * adding a fallback to logger.log issues a very verbose deprecation warning * putting back compatibility with json based web ui --- pokecli.py | 98 +++- pokemongo_bot/__init__.py | 497 +++++++++++++++--- pokemongo_bot/api_wrapper.py | 9 +- pokemongo_bot/cell_workers/base_task.py | 16 + .../cell_workers/catch_lured_pokemon.py | 16 +- .../cell_workers/catch_visible_pokemon.py | 18 +- .../cell_workers/collect_level_up_reward.py | 23 +- pokemongo_bot/cell_workers/evolve_pokemon.py | 44 +- pokemongo_bot/cell_workers/follow_cluster.py | 39 +- pokemongo_bot/cell_workers/follow_path.py | 13 +- pokemongo_bot/cell_workers/follow_spiral.py | 15 +- pokemongo_bot/cell_workers/handle_soft_ban.py | 25 +- pokemongo_bot/cell_workers/incubate_eggs.py | 80 ++- pokemongo_bot/cell_workers/move_to_fort.py | 36 +- .../cell_workers/nickname_pokemon.py | 54 +- .../cell_workers/pokemon_catch_worker.py | 199 +++++-- pokemongo_bot/cell_workers/recycle_items.py | 21 +- pokemongo_bot/cell_workers/sleep_schedule.py | 18 +- pokemongo_bot/cell_workers/spin_fort.py | 98 ++-- .../cell_workers/transfer_pokemon.py | 56 +- .../event_handlers/logging_handler.py | 7 +- .../event_handlers/socketio_handler.py | 28 +- pokemongo_bot/event_manager.py | 26 +- pokemongo_bot/health_record/bot_event.py | 9 +- pokemongo_bot/logger.py | 50 +- pokemongo_bot/socketio_server/app.py | 28 +- pokemongo_bot/test/follow_cluster_test.py | 12 +- pokemongo_bot/walkers/polyline_walker.py | 9 +- pokemongo_bot/websocket_remote_control.py | 13 +- tests/__init__.py | 5 +- ws_server.py | 26 + 31 files changed, 1179 insertions(+), 409 deletions(-) create mode 100755 ws_server.py diff --git a/pokecli.py b/pokecli.py index 7df9369120..94f8b01ee2 100755 --- a/pokecli.py +++ b/pokecli.py @@ -39,21 +39,26 @@ from geopy.exc import GeocoderQuotaExceeded from pokemongo_bot import PokemonGoBot, TreeConfigBuilder -from pokemongo_bot import logger if sys.version_info >= (2, 7, 9): ssl._create_default_https_context = ssl._create_unverified_context +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s [%(name)10s] [%(levelname)s] %(message)s') +logger = logging.getLogger('cli') +logger.setLevel(logging.INFO) def main(): - logger.log('PokemonGO Bot v1.0', 'green') + + logger.info('PokemonGO Bot v1.0') sys.stdout = codecs.getwriter('utf8')(sys.stdout) sys.stderr = codecs.getwriter('utf8')(sys.stderr) config = init_config() if not config: return - logger.log('Configuration initialized', 'yellow') + logger.info('Configuration initialized') finished = False @@ -65,28 +70,56 @@ def main(): bot.workers = tree bot.metrics.capture_stats() - logger.log('Starting PokemonGo Bot....', 'green') + bot.event_manager.emit( + 'bot_start', + sender=bot, + level='info', + formatted='Starting bot...' + ) while True: bot.tick() except KeyboardInterrupt: - logger.log('Exiting PokemonGo Bot', 'red') + bot.event_manager.emit( + 'bot_exit', + sender=bot, + level='info', + formatted='Exiting bot.' + ) finished = True report_summary(bot) - except (NotLoggedInException, ServerBusyOrOfflineException): - logger.log('[x] Error while connecting to the server, please wait %s minutes' % config.reconnecting_timeout, 'red') - time.sleep(config.reconnecting_timeout * 60) + + except NotLoggedInException: + wait_time = config.reconnecting_timeout * 60 + bot.event_manager.emit( + 'api_error', + sender=bot, + level='info', + formmated='Log logged in, reconnecting in {:s}'.format(wait_time) + ) + time.sleep(wait_time) + except ServerBusyOrOfflineException: + bot.event_manager.emit( + 'api_error', + sender=bot, + level='info', + formatted='Server busy or offline' + ) except ServerSideRequestThrottlingException: - logger.log('Server is throttling, reconnecting in 30sec') + bot.event_manager.emit( + 'api_error', + sender=bot, + level='info', + formatted='Server is throttling, reconnecting in 30 seconds' + ) time.sleep(30) except GeocoderQuotaExceeded: - logger.log('[x] The given maps api key has gone over the requests limit.', 'red') - finished = True - except: + raise "Google Maps API key over requests limit." + except Exception as e: # always report session summary and then raise exception report_summary(bot) - raise + raise e def report_summary(bot): if bot.metrics.start_time is None: @@ -94,22 +127,21 @@ def report_summary(bot): metrics = bot.metrics metrics.capture_stats() - logger.log('') - logger.log('Ran for {}'.format(metrics.runtime()), 'cyan') - logger.log('Total XP Earned: {} Average: {:.2f}/h'.format(metrics.xp_earned(), metrics.xp_per_hour()), 'cyan') - logger.log('Travelled {:.2f}km'.format(metrics.distance_travelled()), 'cyan') - logger.log('Visited {} stops'.format(metrics.visits['latest'] - metrics.visits['start']), 'cyan') - logger.log('Encountered {} pokemon, {} caught, {} released, {} evolved, {} never seen before' + logger.info('') + logger.info('Ran for {}'.format(metrics.runtime())) + logger.info('Total XP Earned: {} Average: {:.2f}/h'.format(metrics.xp_earned(), metrics.xp_per_hour())) + logger.info('Travelled {:.2f}km'.format(metrics.distance_travelled())) + logger.info('Visited {} stops'.format(metrics.visits['latest'] - metrics.visits['start'])) + logger.info('Encountered {} pokemon, {} caught, {} released, {} evolved, {} never seen before' .format(metrics.num_encounters(), metrics.num_captures(), metrics.releases, - metrics.num_evolutions(), metrics.num_new_mons()), 'cyan') - logger.log('Threw {} pokeball{}'.format(metrics.num_throws(), '' if metrics.num_throws() == 1 else 's'), - 'cyan') - logger.log('Earned {} Stardust'.format(metrics.earned_dust()), 'cyan') - logger.log('') + metrics.num_evolutions(), metrics.num_new_mons())) + logger.info('Threw {} pokeball{}'.format(metrics.num_throws(), '' if metrics.num_throws() == 1 else 's')) + logger.info('Earned {} Stardust'.format(metrics.earned_dust())) + logger.info('') if metrics.highest_cp is not None: - logger.log('Highest CP Pokemon: {}'.format(metrics.highest_cp['desc']), 'cyan') + logger.info('Highest CP Pokemon: {}'.format(metrics.highest_cp['desc'])) if metrics.most_perfect is not None: - logger.log('Most Perfect Pokemon: {}'.format(metrics.most_perfect['desc']), 'cyan') + logger.info('Most Perfect Pokemon: {}'.format(metrics.most_perfect['desc'])) def init_config(): parser = argparse.ArgumentParser() @@ -126,11 +158,11 @@ def init_config(): with open(config_arg) as data: load.update(json.load(data)) elif os.path.isfile(config_file): - logger.log('No config argument specified, checking for /configs/config.json', 'yellow') + logger.info('No config argument specified, checking for /configs/config.json') with open(config_file) as data: load.update(json.load(data)) else: - logger.log('Error: No /configs/config.json or specified config', 'red') + logger.info('Error: No /configs/config.json or specified config') # Read passed in Arguments required = lambda x: not x in load @@ -228,7 +260,15 @@ def init_config(): type=str, default=None ) - + add_config( + parser, + load, + short_flag="-e", + long_flag="--show_events", + help="Show events", + type=bool, + default=False + ) add_config( parser, load, diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index c5cf3d4614..045abd82e9 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals import datetime import json @@ -14,7 +15,6 @@ from pgoapi.utilities import f2i, get_cell_ids import cell_workers -import logger from api_wrapper import ApiWrapper from cell_workers.utils import distance from event_manager import EventManager @@ -23,6 +23,7 @@ from metrics import Metrics from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler from pokemongo_bot.socketio_server.runner import SocketIoRunner +from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl from worker_result import WorkerResult from tree_config_builder import ConfigException, TreeConfigBuilder @@ -52,11 +53,13 @@ def __init__(self, config): self.start_position = None self.last_map_object = None self.last_time_map_object = 0 + self.logger = logging.getLogger(type(self).__name__) # Make our own copy of the workers for this instance self.workers = [] def start(self): + self._setup_event_system() self._setup_logging() self._setup_api() @@ -64,15 +67,26 @@ def start(self): def _setup_event_system(self): handlers = [LoggingHandler()] - if self.config.websocket_server: - websocket_handler = SocketIoHandler(self.config.websocket_server_url) - handlers.append(websocket_handler) - + if self.config.websocket_server_url: if self.config.websocket_start_embedded_server: self.sio_runner = SocketIoRunner(self.config.websocket_server_url) self.sio_runner.start_listening_async() + websocket_handler = SocketIoHandler( + self, + self.config.websocket_server_url + ) + handlers.append(websocket_handler) + + if self.config.websocket_remote_control: + remote_control = WebsocketRemoteControl(self).start() + + self.event_manager = EventManager(*handlers) + self._register_events() + if self.config.show_events: + self.event_manager.event_report() + sys.exit(1) # Registering event: # self.event_manager.register_event("location", parameters=['lat', 'lng']) @@ -81,6 +95,293 @@ def _setup_event_system(self): # message: : # self.event_manager.emit('location', 'level'='info', data={'lat': 1, 'lng':1}), + def _register_events(self): + self.event_manager.register_event( + 'location_found', + parameters=('position', 'location') + ) + self.event_manager.register_event('api_error') + self.event_manager.register_event('config_error') + + self.event_manager.register_event('login_started') + self.event_manager.register_event('login_failed') + self.event_manager.register_event('login_successful') + + self.event_manager.register_event('set_start_location') + self.event_manager.register_event('load_cached_location') + self.event_manager.register_event('location_cache_ignored') + self.event_manager.register_event( + 'position_update', + parameters=( + 'current_position', + 'last_position', + 'distance', # optional + 'distance_unit' # optional + ) + ) + self.event_manager.register_event('location_cache_error') + + self.event_manager.register_event('bot_start') + self.event_manager.register_event('bot_exit') + + # sleep stuff + self.event_manager.register_event( + 'next_sleep', + parameters=('time',) + ) + self.event_manager.register_event( + 'bot_sleep', + parameters=('time_in_seconds',) + ) + + # fort stuff + self.event_manager.register_event( + 'spun_fort', + parameters=( + 'fort_id', + 'latitude', + 'longitude' + ) + ) + self.event_manager.register_event( + 'lured_pokemon_found', + parameters=( + 'fort_id', + 'fort_name', + 'encounter_id', + 'latitude', + 'longitude' + ) + ) + self.event_manager.register_event( + 'moving_to_fort', + parameters=( + 'fort_name', + 'distance' + ) + ) + self.event_manager.register_event( + 'moving_to_lured_fort', + parameters=( + 'fort_name', + 'distance', + 'lure_distance' + ) + ) + self.event_manager.register_event( + 'spun_pokestop', + parameters=( + 'pokestop', 'exp', 'items' + ) + ) + self.event_manager.register_event( + 'pokestop_empty', + parameters=('pokestop',) + ) + self.event_manager.register_event( + 'pokestop_out_of_range', + parameters=('pokestop',) + ) + self.event_manager.register_event( + 'pokestop_on_cooldown', + parameters=('pokestop', 'minutes_left') + ) + self.event_manager.register_event( + 'unknown_spin_result', + parameters=('status_code',) + ) + self.event_manager.register_event('pokestop_searching_too_often') + self.event_manager.register_event('arrived_at_fort') + + # pokemon stuff + self.event_manager.register_event( + 'catchable_pokemon', + parameters=( + 'pokemon_id', + 'spawn_point_id', + 'encounter_id', + 'latitude', + 'longitude', + 'expiration_timestamp_ms' + ) + ) + self.event_manager.register_event( + 'pokemon_appeared', + parameters=( + 'pokemon', + 'cp', + 'iv', + 'iv_display', + ) + ) + self.event_manager.register_event( + 'pokemon_catch_rate', + parameters=( + 'catch_rate', + 'berry_name', + 'berry_count' + ) + ) + self.event_manager.register_event( + 'threw_berry', + parameters=( + 'berry_name', + 'new_catch_rate' + ) + ) + self.event_manager.register_event( + 'threw_pokeball', + parameters=( + 'pokeball', + 'success_percentage', + 'count_left' + ) + ) + self.event_manager.register_event( + 'pokemon_fled', + parameters=('pokemon',) + ) + self.event_manager.register_event( + 'pokemon_vanished', + parameters=('pokemon',) + ) + self.event_manager.register_event( + 'pokemon_caught', + parameters=( + 'pokemon', + 'cp', 'iv', 'iv_display', 'exp' + ) + ) + self.event_manager.register_event( + 'pokemon_evolved', + parameters=('pokemon', 'iv', 'cp') + ) + self.event_manager.register_event( + 'pokemon_evolve_fail', + parameters=('pokemon',) + ) + 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') + + + # level up stuff + self.event_manager.register_event( + 'level_up', + parameters=( + 'previous_level', + 'current_level' + ) + ) + self.event_manager.register_event( + 'level_up_reward', + parameters=('items',) + ) + + # lucky egg + self.event_manager.register_event( + 'used_lucky_egg', + parameters=('amount_left',) + ) + self.event_manager.register_event('lucky_egg_error') + + # softban + self.event_manager.register_event('softban') + self.event_manager.register_event('softban_fix') + self.event_manager.register_event('softban_fix_done') + + # egg incubating + self.event_manager.register_event( + 'incubate_try', + parameters=( + 'incubator_id', + 'egg_id' + ) + ) + self.event_manager.register_event( + 'incubate', + parameters=('distance_in_km',) + ) + self.event_manager.register_event( + 'next_egg_incubates', + parameters=('distance_in_km',) + ) + self.event_manager.register_event('incubator_already_used') + self.event_manager.register_event('egg_already_incubating') + self.event_manager.register_event( + 'egg_hatched', + parameters=( + 'pokemon', + 'cp', 'iv', 'exp', 'stardust', 'candy' + ) + ) + + # discard item + self.event_manager.register_event( + 'item_discarded', + parameters=( + 'amount', 'item', 'maximum' + ) + ) + self.event_manager.register_event( + 'item_discard_fail', + parameters=('item',) + ) + + # inventory + self.event_manager.register_event('inventory_full') + + # release + self.event_manager.register_event( + 'keep_best_release', + parameters=( + 'amount', 'pokemon', 'criteria' + ) + ) + self.event_manager.register_event( + 'future_pokemon_release', + parameters=( + 'pokemon', 'cp', 'iv', 'below_iv', 'below_cp', 'cp_iv_logic' + ) + ) + self.event_manager.register_event( + 'pokemon_release', + parameters=('pokemon', 'cp', 'iv') + ) + + # polyline walker + self.event_manager.register_event( + 'polyline_request', + parameters=('url',) + ) + + # cluster + self.event_manager.register_event( + 'found_cluster', + parameters=( + 'num_points', 'forts', 'radius', 'distance' + ) + ) + self.event_manager.register_event( + 'arrived_at_cluster', + parameters=( + 'forts', 'radius' + ) + ) + + # rename + self.event_manager.register_event( + 'rename_pokemon', + parameters=( + 'old_name', 'current_name' + ) + ) + self.event_manager.register_event( + 'pokemon_nickname_invalid', + parameters=('nickname',) + ) + self.event_manager.register_event('unset_pokemon_nickname') + def tick(self): self.cell = self.get_meta_cell() self.tick_count += 1 @@ -170,7 +471,7 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): 'cells': cells }, outfile) except IOError as e: - logger.log('[x] Error while opening location file: %s' % e, 'red') + self.logger.info('[x] Error while opening location file: %s' % e) user_data_lastlocation = os.path.join( 'data', 'last-location-%s.json' % self.config.username @@ -179,7 +480,7 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): with open(user_data_lastlocation, 'w') as outfile: json.dump({'lat': lat, 'lng': lng, 'start_position': self.start_position}, outfile) except IOError as e: - logger.log('[x] Error while opening location file: %s' % e, 'red') + self.logger.info('[x] Error while opening location file: %s' % e) def find_close_cells(self, lat, lng): cellid = get_cell_ids(lat, lng) @@ -204,14 +505,11 @@ def find_close_cells(self, lat, lng): return map_cells def _setup_logging(self): - self.log = logging.getLogger(__name__) # log settings # log format - logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s [%(name)10s] [%(levelname)5s] %(message)s') if self.config.debug: + log_level = logging.DEBUG logging.getLogger("requests").setLevel(logging.DEBUG) logging.getLogger("websocket").setLevel(logging.DEBUG) logging.getLogger("socketio").setLevel(logging.DEBUG) @@ -220,6 +518,7 @@ def _setup_logging(self): logging.getLogger("pgoapi").setLevel(logging.DEBUG) logging.getLogger("rpc_api").setLevel(logging.DEBUG) else: + log_level = logging.ERROR logging.getLogger("requests").setLevel(logging.ERROR) logging.getLogger("websocket").setLevel(logging.ERROR) logging.getLogger("socketio").setLevel(logging.ERROR) @@ -228,20 +527,24 @@ def _setup_logging(self): logging.getLogger("pgoapi").setLevel(logging.ERROR) logging.getLogger("rpc_api").setLevel(logging.ERROR) + logging.basicConfig( + level=log_level, + format='%(asctime)s [%(name)10s] [%(levelname)s] %(message)s' + ) def check_session(self, position): # Check session expiry if self.api._auth_provider and self.api._auth_provider._ticket_expire: # prevent crash if return not numeric value if not self.is_numeric(self.api._auth_provider._ticket_expire): - logger.log("Ticket expired value is not numeric", 'yellow') + self.logger.info("Ticket expired value is not numeric", 'yellow') return remaining_time = \ self.api._auth_provider._ticket_expire / 1000 - time.time() if remaining_time < 60: - logger.log("Session stale, re-logging in", 'yellow') + self.logger.info("Session stale, re-logging in", 'yellow') position = self.position self.api = ApiWrapper() self.position = position @@ -256,7 +559,12 @@ def is_numeric(s): return False def login(self): - logger.log('Attempting login to Pokemon Go.', 'white') + self.event_manager.emit( + 'login_started', + sender=self, + level='info', + formatted="Login procedure started." + ) lat, lng = self.position[0:2] self.api.set_position(lat, lng, 0) @@ -265,11 +573,20 @@ def login(self): 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') + self.event_manager.emit( + 'login_failed', + sender=self, + level='info', + formatted="Login error, server busy. Waiting 10 seconds to try again." + ) time.sleep(10) - logger.log('Login to Pokemon Go successful.', 'green') + self.event_manager.emit( + 'login_successful', + sender=self, + level='info', + formatted="Login successful." + ) def _setup_api(self): # instantiate pgoapi @@ -279,12 +596,11 @@ def _setup_api(self): self._set_starting_position() self.login() - # chain subrequests (methods) into one RPC call self._print_character_info() - logger.log('') + self.logger.info('') self.update_inventory() # send empty map_cells and then our position self.update_web_location() @@ -301,7 +617,7 @@ def _print_character_info(self): self._player = response_dict['responses']['GET_PLAYER']['player_data'] player = self._player else: - logger.log( + self.logger.info( "The API didn't return player info, servers are unstable - " "retrying.", 'red' ) @@ -321,56 +637,56 @@ def _print_character_info(self): pokecoins = player['currencies'][0]['amount'] if 'amount' in player['currencies'][1]: stardust = player['currencies'][1]['amount'] - logger.log('') - logger.log('--- {username} ---'.format(**player), 'cyan') + self.logger.info('') + self.logger.info('--- {username} ---'.format(**player)) self.get_player_info() - logger.log( + self.logger.info( 'Pokemon Bag: {}/{}'.format( self.get_inventory_count('pokemon'), player['max_pokemon_storage'] - ), 'cyan' + ) ) - logger.log( + self.logger.info( 'Items: {}/{}'.format( self.get_inventory_count('item'), player['max_item_storage'] - ), 'cyan' + ) ) - logger.log( + self.logger.info( 'Stardust: {}'.format(stardust) + - ' | Pokecoins: {}'.format(pokecoins), 'cyan' + ' | Pokecoins: {}'.format(pokecoins) ) # Items Output - logger.log( + self.logger.info( 'PokeBalls: ' + str(items_stock[1]) + ' | GreatBalls: ' + str(items_stock[2]) + - ' | UltraBalls: ' + str(items_stock[3]), 'cyan') + ' | UltraBalls: ' + str(items_stock[3])) - logger.log( + self.logger.info( 'RazzBerries: ' + str(items_stock[701]) + ' | BlukBerries: ' + str(items_stock[702]) + - ' | NanabBerries: ' + str(items_stock[703]), 'cyan') + ' | NanabBerries: ' + str(items_stock[703])) - logger.log( + self.logger.info( 'LuckyEgg: ' + str(items_stock[301]) + ' | Incubator: ' + str(items_stock[902]) + - ' | TroyDisk: ' + str(items_stock[501]), 'cyan') + ' | TroyDisk: ' + str(items_stock[501])) - logger.log( + self.logger.info( 'Potion: ' + str(items_stock[101]) + ' | SuperPotion: ' + str(items_stock[102]) + - ' | HyperPotion: ' + str(items_stock[103]), 'cyan') + ' | HyperPotion: ' + str(items_stock[103])) - logger.log( + self.logger.info( 'Incense: ' + str(items_stock[401]) + ' | IncenseSpicy: ' + str(items_stock[402]) + - ' | IncenseCool: ' + str(items_stock[403]), 'cyan') + ' | IncenseCool: ' + str(items_stock[403])) - logger.log( + self.logger.info( 'Revive: ' + str(items_stock[201]) + - ' | MaxRevive: ' + str(items_stock[202]), 'cyan') + ' | MaxRevive: ' + str(items_stock[202])) - logger.log('') + self.logger.info('') def use_lucky_egg(self): return self.api.use_item_xp_boost(item_id=301) @@ -450,6 +766,13 @@ def _all_items_inventory_count(self, inventory_dict): def _set_starting_position(self): + self.event_manager.emit( + 'set_start_location', + sender=self, + level='info', + formatted='Setting start location.' + ) + has_position = False if self.config.test: @@ -457,23 +780,51 @@ def _set_starting_position(self): return if self.config.location: - location_str = self.config.location.encode('utf-8') - location = (self.get_pos_by_name(location_str.replace(" ", ""))) + location_str = self.config.location + location = self.get_pos_by_name(location_str.replace(" ", "")) + msg = "Location found: {location} {position}" + self.event_manager.emit( + 'location_found', + sender=self, + level='info', + formatted=msg, + data={ + 'location': location_str, + 'position': location + } + ) + self.api.set_position(*location) + + self.event_manager.emit( + 'position_update', + sender=self, + level='info', + formatted="Now at {current_position}", + data={ + 'current_position': self.position, + 'last_position': '', + 'distance': '', + 'distance_unit': '' + } + ) + self.start_position = self.position - logger.log('') - logger.log('Location Found: {}'.format(location_str)) - logger.log('GeoPosition: {}'.format(self.position)) - logger.log('') + has_position = True if self.config.location_cache: try: # save location flag used to pull the last known location from # the location.json - logger.log('[x] Parsing cached location...') + self.event_manager.emit( + 'load_cached_location', + sender=self, + level='debug', + formatted='Loading cached location...' + ) with open('data/last-location-%s.json' % - self.config.username) as f: + self.config.username) as f: location_json = json.load(f) location = ( location_json['lat'], @@ -487,22 +838,28 @@ def _set_starting_position(self): # Start position has to have been set on a previous run to do this check if last_start_position and last_start_position != self.start_position: - logger.log('[x] Last location flag used but with a stale starting location', 'yellow') - logger.log('[x] Using new starting location, {}'.format(self.position)) + msg = 'Going to a new place, ignoring cached location.' + self.event_manager.emit( + 'location_cache_ignored', + sender=self, + level='debug', + formatted=msg + ) return self.api.set_position(*location) - - logger.log('') - logger.log( - '[x] Last location flag used. Overriding passed in location' - ) - logger.log( - '[x] Last in-game location was set as: {}'.format( - self.position - ) + self.event_manager.emit( + 'position_update', + sender=self, + level='debug', + formatted='Loaded location {current_position} from cache', + data={ + 'current_position': location, + 'last_position': '', + 'distance': '', + 'distance_unit': '' + } ) - logger.log('') has_position = True except Exception: @@ -510,9 +867,11 @@ def _set_starting_position(self): sys.exit( "No cached Location. Please specify initial location." ) - logger.log( - '[x] Parsing cached location failed, try to use the ' - 'initial location...' + self.event_manager.emit( + 'location_cache_error', + sender=self, + level='debug', + formatted='Parsing cached location failed.' ) def get_pos_by_name(self, location_name): @@ -524,7 +883,7 @@ def get_pos_by_name(self, location_name): if len(possible_coordinates) == 2: # 2 matches, this must be a coordinate. We'll bypass the Google # geocode so we keep the exact location. - logger.log( + self.logger.info( '[x] Coordinates found in passed in location, ' 'not geocoding.' ) @@ -578,22 +937,22 @@ def get_player_info(self): nextlvlxp = (int(playerdata.get('next_level_xp', 0)) - int(playerdata.get('experience', 0))) if 'level' in playerdata and 'experience' in playerdata: - logger.log( + self.logger.info( 'Level: {level}'.format( **playerdata) + ' (Next Level: {} XP)'.format( nextlvlxp) + ' (Total: {experience} XP)' - ''.format(**playerdata), 'cyan') + ''.format(**playerdata)) if 'pokemons_captured' in playerdata and 'poke_stop_visits' in playerdata: - logger.log( + self.logger.info( 'Pokemon Captured: ' '{pokemons_captured}'.format( **playerdata) + ' | Pokestops Visited: ' '{poke_stop_visits}'.format( - **playerdata), 'cyan') + **playerdata)) def has_space_for_loot(self): number_of_things_gained_by_stop = 5 diff --git a/pokemongo_bot/api_wrapper.py b/pokemongo_bot/api_wrapper.py index 17612aceb6..324d645043 100644 --- a/pokemongo_bot/api_wrapper.py +++ b/pokemongo_bot/api_wrapper.py @@ -1,4 +1,5 @@ import time +import logging from pgoapi.exceptions import (ServerSideRequestThrottlingException, NotLoggedInException, ServerBusyOrOfflineException, @@ -7,7 +8,6 @@ from pgoapi.pgoapi import PGoApi, PGoApiRequest, RpcApi from pgoapi.protos.POGOProtos.Networking.Requests_pb2 import RequestType -import pokemongo_bot.logger as logger from human_behaviour import sleep class ApiWrapper(PGoApi): @@ -42,6 +42,7 @@ def login(self, *args): class ApiRequest(PGoApiRequest): def __init__(self, *args): PGoApiRequest.__init__(self, *args) + self.logger = logging.getLogger(__name__) self.request_callers = [] self.last_api_request_time = None self.requests_per_seconds = 2 @@ -119,16 +120,16 @@ def call(self, max_retry=15): if should_unexpected_response_retry: unexpected_response_retry += 1 if unexpected_response_retry >= 5: - logger.log('Server is not responding correctly to our requests. Waiting for 30 seconds to reconnect.', 'red') + self.logger.warning('Server is not responding correctly to our requests. Waiting for 30 seconds to reconnect.') sleep(30) else: sleep(2) continue - + if not self.is_response_valid(result, request_callers): try_cnt += 1 if try_cnt > 3: - logger.log('Server seems to be busy or offline - try again - {}/{}'.format(try_cnt, max_retry), 'red') + self.logger.warning('Server seems to be busy or offline - try again - {}/{}'.format(try_cnt, max_retry)) if try_cnt >= max_retry: raise ServerBusyOrOfflineException() sleep(1) diff --git a/pokemongo_bot/cell_workers/base_task.py b/pokemongo_bot/cell_workers/base_task.py index 4d73a68443..ac48b9a676 100644 --- a/pokemongo_bot/cell_workers/base_task.py +++ b/pokemongo_bot/cell_workers/base_task.py @@ -1,8 +1,13 @@ +import logging + + class BaseTask(object): + 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): @@ -10,5 +15,16 @@ def _validate_work_exists(self): 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 diff --git a/pokemongo_bot/cell_workers/catch_lured_pokemon.py b/pokemongo_bot/cell_workers/catch_lured_pokemon.py index d25a8f1728..bf2d45bb4b 100644 --- a/pokemongo_bot/cell_workers/catch_lured_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_lured_pokemon.py @@ -1,8 +1,11 @@ -from pokemongo_bot import logger +# -*- 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.cell_workers.base_task import BaseTask + class CatchLuredPokemon(BaseTask): def work(self): lured_pokemon = self.get_lured_pokemon() @@ -24,14 +27,21 @@ def get_lured_pokemon(self): encounter_id = fort.get('lure_info', {}).get('encounter_id', None) if encounter_id: - logger.log('Lured pokemon at fort {}'.format(fort_name)) - return { + result = { 'encounter_id': encounter_id, 'fort_id': fort['id'], + 'fort_name': fort_name, 'latitude': fort['latitude'], 'longitude': fort['longitude'] } + self.emit_event( + 'lured_pokemon_found', + formatted='Lured pokemon at fort {fort_name} ({fort_id})', + data=result + ) + return result + return False def catch_pokemon(self, pokemon): diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py index 7fe9df6cb4..c9c6147c0a 100644 --- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -1,6 +1,5 @@ import json -from pokemongo_bot import logger from pokemongo_bot.cell_workers.base_task import BaseTask from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker from utils import distance @@ -9,17 +8,28 @@ class CatchVisiblePokemon(BaseTask): def work(self): if 'catchable_pokemons' in self.bot.cell and len(self.bot.cell['catchable_pokemons']) > 0: - logger.log('Something rustles nearby!') # Sort all by distance from current pos- eventually this should # build graph & A* it self.bot.cell['catchable_pokemons'].sort( key= - lambda x: distance(self.bot.position[0], self.bot.position[1], x['latitude'], x['longitude'])) + lambda x: distance(self.bot.position[0], self.bot.position[1], x['latitude'], x['longitude']) + ) - user_web_catchable = 'web/catchable-%s.json' % 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', + data={ + 'pokemon_id': pokemon['pokemon_id'], + 'spawn_point_id': pokemon['spawn_point_id'], + 'encounter_id': pokemon['encounter_id'], + 'latitude': pokemon['latitude'], + 'longitude': pokemon['longitude'], + 'expiration_timestamp_ms': pokemon['expiration_timestamp_ms'], + } + ) return self.catch_pokemon(self.bot.cell['catchable_pokemons'].pop(0)) diff --git a/pokemongo_bot/cell_workers/collect_level_up_reward.py b/pokemongo_bot/cell_workers/collect_level_up_reward.py index 912a97d197..304818fe2b 100644 --- a/pokemongo_bot/cell_workers/collect_level_up_reward.py +++ b/pokemongo_bot/cell_workers/collect_level_up_reward.py @@ -1,4 +1,3 @@ -from pokemongo_bot import logger from pokemongo_bot.cell_workers.base_task import BaseTask @@ -19,7 +18,14 @@ def work(self): self._collect_level_reward() # level up situation elif self.current_level > self.previous_level: - logger.log('Level up from {} to {}!'.format(self.previous_level, self.current_level), 'green') + self.emit_event( + 'level_up', + formatted='Level up from {previous_level} to {current_level}', + data={ + 'previous_level': self.previous_level, + 'current_level': self.current_level + } + ) self._collect_level_reward() self.previous_level = self.current_level @@ -32,14 +38,19 @@ def _collect_level_reward(self): .get('LEVEL_UP_REWARDS', {}) .get('items_awarded', [])) - if data: - logger.log('Collected level up rewards:', 'green') - for item in data: if 'item_id' in item and str(item['item_id']) in self.bot.item_list: got_item = self.bot.item_list[str(item['item_id'])] + item['name'] = got_item count = 'item_count' in item and item['item_count'] or 0 - logger.log('{} x {}'.format(got_item, count), 'green') + + self.emit_event( + 'level_up_reward', + formatted='Received level up reward: {items}', + data={ + 'items': data + } + ) def _get_current_level(self): level = 0 diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 52013ebbcc..91c9c807cf 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -1,13 +1,14 @@ -from pokemongo_bot import logger from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.item_list import Item from pokemongo_bot.cell_workers.base_task import BaseTask + class EvolvePokemon(BaseTask): + def initialize(self): self.api = self.bot.api self.evolve_all = self.config.get('evolve_all', []) - self.evolve_speed = self.config.get('evolve_speed', 3.7) + self.evolve_speed = self.config.get('evolve_speed', 2) self.first_evolve_by = self.config.get('first_evolve_by', 'cp') self.evolve_above_cp = self.config.get('evolve_above_cp', 500) self.evolve_above_iv = self.config.get('evolve_above_iv', 0.8) @@ -35,14 +36,9 @@ def work(self): cache = {} candy_list = self._get_candy_list(inventory_items) - evolved = 0 for pokemon in evolve_list: - if self._can_evolve(pokemon, candy_list, cache) \ - and self._execute_pokemon_evolve(pokemon, candy_list, cache): - evolved += 1 - - if evolved > 0: - logger.log('Evolved {} pokemon!'.format(evolved)) + if self._can_evolve(pokemon, candy_list, cache): + self._execute_pokemon_evolve(pokemon, candy_list, cache) def _should_run(self): if not self.evolve_all or self.evolve_all[0] == 'none': @@ -56,19 +52,31 @@ def _should_run(self): # Make sure the user has a lucky egg and skip if not if lucky_egg_count > 0: - logger.log('Using lucky egg ... you have {}'.format(lucky_egg_count)) response_dict_lucky_egg = self.bot.use_lucky_egg() if response_dict_lucky_egg: result = response_dict_lucky_egg.get('responses', {}).get('USE_ITEM_XP_BOOST', {}).get('result', 0) if result is 1: # Request success - logger.log('Successfully used lucky egg... ({} left!)'.format(lucky_egg_count - 1), 'green') + self.emit_event( + 'used_lucky_egg', + formmated='Used lucky egg ({amount_left} left).', + data={ + 'amount_left': lucky_egg_count - 1 + } + ) return True else: - logger.log('Failed to use lucky egg!', 'red') + self.emit_event( + 'lucky_egg_error', + level='error', + formatted='Failed to use lucky egg!' + ) return False else: # Skipping evolve so they aren't wasted - logger.log('No lucky eggs... skipping evolve!', 'yellow') + self.emit_event( + 'skip_evolve', + formatted='Skipping evolve because has no lucky egg.' + ) return False def _get_candy_list(self, inventory_items): @@ -136,7 +144,15 @@ def _execute_pokemon_evolve(self, pokemon, candy_list, cache): response_dict = self.api.evolve_pokemon(pokemon_id=pokemon_id) if response_dict.get('responses', {}).get('EVOLVE_POKEMON', {}).get('result', 0) == 1: - logger.log('Successfully evolved {} with {} CP and {} IV!'.format(pokemon_name, pokemon_cp, pokemon_iv)) + 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 + } + ) candy_list[pokemon["candies_family"]] -= pokemon["candies_amount"] sleep(self.evolve_speed) return True diff --git a/pokemongo_bot/cell_workers/follow_cluster.py b/pokemongo_bot/cell_workers/follow_cluster.py index 9c845404bd..02d3880a7e 100644 --- a/pokemongo_bot/cell_workers/follow_cluster.py +++ b/pokemongo_bot/cell_workers/follow_cluster.py @@ -1,16 +1,14 @@ -from pokemongo_bot import logger from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.cell_workers.utils import distance from pokemongo_bot.cell_workers.utils import find_biggest_cluster +from pokemongo_bot.cell_workers.base_task import BaseTask +class FollowCluster(BaseTask): -class FollowCluster(object): - def __init__(self, bot, config): - self.bot = bot + def initialize(self): self.is_at_destination = False self.announced = False self.dest = None - self.config = config self._process_config() def _process_config(self): @@ -39,11 +37,21 @@ def work(self): cnt = self.dest['num_points'] if not self.is_at_destination: + msg = log_lure_avail_str + ( + "Move to destiny {num_points}. {forts} " + "pokestops will be in range of {radius}. Walking {distance}m." + ) + self.emit_event( + 'found_cluster', + formatted=msg, + data={ + 'num_points': cnt, + 'forts': log_lured_str, + 'radius': str(self.radius), + 'distance': str(distance(self.bot.position[0], self.bot.position[1], lat, lng)) + } + ) - log_str = log_lure_avail_str + 'Move to destiny. ' + str(cnt) + ' ' + log_lured_str + \ - 'pokestops will be in range of ' + str(self.radius) + 'm. Arrive in ' \ - + str(distance(self.bot.position[0], self.bot.position[1], lat, lng)) + 'm.' - logger.log(log_str) self.announced = False if self.bot.config.walk > 0: @@ -61,14 +69,17 @@ def work(self): self.bot.api.set_position(lat, lng) elif not self.announced: - log_str = 'Arrived at destiny. ' + str(cnt) + ' pokestops are in range of ' \ - + str(self.radius) + 'm.' - logger.log(log_str) + self.emit_event( + 'arrived_at_cluster', + formatted="Arrived at cluster. {forts} are in a range of {radius}m radius.", + data={ + 'forts': str(cnt), + 'radius': self.radius + } + ) self.announced = True else: lat = self.bot.position[0] lng = self.bot.position[1] return [lat, lng] - - diff --git a/pokemongo_bot/cell_workers/follow_path.py b/pokemongo_bot/cell_workers/follow_path.py index 81a5d19695..04eb817593 100644 --- a/pokemongo_bot/cell_workers/follow_path.py +++ b/pokemongo_bot/cell_workers/follow_path.py @@ -3,7 +3,6 @@ import gpxpy import gpxpy.gpx import json -import pokemongo_bot.logger as logger from pokemongo_bot.cell_workers.base_task import BaseTask from pokemongo_bot.cell_workers.utils import distance, i2f, format_dist from pokemongo_bot.human_behaviour import sleep @@ -34,11 +33,17 @@ def load_json(self): with open(self.path_file) as data_file: points=json.load(data_file) # Replace Verbal Location with lat&lng. - logger.log("Resolving Navigation Paths (GeoLocating Strings)") for index, point in enumerate(points): - if self.bot.config.debug: - logger.log("Resolving Point {} - {}".format(index, point)) point_tuple = self.bot.get_pos_by_name(point['location']) + self.emit_event( + 'location_found', + level='debug', + formatted="Location found: {location} {position}", + data={ + 'location': point, + 'position': point_tuple + } + ) points[index] = self.lat_lng_tuple_to_dict(point_tuple) return points diff --git a/pokemongo_bot/cell_workers/follow_spiral.py b/pokemongo_bot/cell_workers/follow_spiral.py index bda348490a..28b548d1ca 100644 --- a/pokemongo_bot/cell_workers/follow_spiral.py +++ b/pokemongo_bot/cell_workers/follow_spiral.py @@ -3,7 +3,6 @@ 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 @@ -84,10 +83,16 @@ def work(self): ) if self.cnt == 1: - logger.log( - 'Walking from ' + str((self.bot.api._position_lat, - self.bot.api._position_lng)) + " to " + str([point['lat'], point['lng']]) + " " + format_dist(dist, - self.bot.config.distance_unit)) + self.emit_event( + 'position_update', + formatted="Walking from {last_position} to {current_position} ({distance} {distance_unit})", + data={ + 'last_position': self.bot.position, + 'current_position': (point['lat'], point['lng'], 0), + 'distance': dist, + 'distance_unit': 'm' + } + ) if step_walker.step(): step_walker = None diff --git a/pokemongo_bot/cell_workers/handle_soft_ban.py b/pokemongo_bot/cell_workers/handle_soft_ban.py index 25c64f60e9..e266c5c377 100644 --- a/pokemongo_bot/cell_workers/handle_soft_ban.py +++ b/pokemongo_bot/cell_workers/handle_soft_ban.py @@ -2,7 +2,6 @@ from pgoapi.utilities import f2i -from pokemongo_bot import logger from pokemongo_bot.constants import Constants from pokemongo_bot.cell_workers.base_task import BaseTask from pokemongo_bot.cell_workers import MoveToFort @@ -18,9 +17,7 @@ def work(self): forts = self.bot.get_forts(order_by_distance=True) if len(forts) == 0: - logger.log('Found no forts to reset softban, skipping...', 'red') return - logger.log('Got softban, fixing...', 'yellow') fort_distance = distance( self.bot.position[0], @@ -37,13 +34,17 @@ def work(self): return WorkerResult.RUNNING else: spins = randint(50,60) - logger.log('Starting %s spins...' % spins) + self.emit_event( + 'softban_fix', + formatted='Fixing softban.' + ) for i in xrange(spins): - if (i + 1) % 10 == 0: - logger.log('Spin #{}'.format(str(i+1))) self.spin_fort(forts[0]) self.bot.softban = False - logger.log('Softban should be fixed.') + self.emit_event( + 'softban_fix_done', + formatted='Softban should be fixed' + ) def spin_fort(self, fort): self.bot.api.fort_search( @@ -53,6 +54,16 @@ def spin_fort(self, fort): player_latitude=f2i(self.bot.position[0]), player_longitude=f2i(self.bot.position[1]) ) + self.bot.event_handler.emit( + 'spun_fort', + level='debug', + formatted="Spun fort {fort_id}", + data={ + 'fort_id': fort_id, + 'lat': fort['latitude'], + 'lng': fort['longitude'] + } + ) 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 a1e3ce351f..9e21b0d280 100644 --- a/pokemongo_bot/cell_workers/incubate_eggs.py +++ b/pokemongo_bot/cell_workers/incubate_eggs.py @@ -1,4 +1,3 @@ -from pokemongo_bot import logger from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.cell_workers.base_task import BaseTask @@ -31,7 +30,13 @@ def work(self): if km_left <= 0: self._hatch_eggs() else: - logger.log('[x] Current egg hatches in {:.2f} km'.format(km_left),'yellow') + self.emit_event( + 'next_egg_incubates', + formatted='Next egg incubates in {distance_in_km:.2f} km', + data={ + 'distance_in_km': km_left + } + ) IncubateEggs.last_km_walked = self.km_walked sorting = self.longer_eggs_first @@ -45,8 +50,15 @@ def _apply_incubators(self): for egg in self.eggs: if egg["used"] or egg["km"] == -1: continue - if self.bot.config.debug: - logger.log('[x] Attempting to apply incubator {} to egg {}'.format(incubator['id'], egg['id'])) + self.emit_event( + 'incubate_try', + level='debug', + formatted="Attempting to apply incubator {incubator_id} to egg {egg_id}", + data={ + 'incubator_id': incubator['id'], + 'egg_id': egg['id'] + } + ) ret = self.bot.api.use_item_egg_incubator( item_id=incubator["id"], pokemon_id=egg["id"] @@ -54,18 +66,30 @@ def _apply_incubators(self): if ret: code = ret.get("responses", {}).get("USE_ITEM_EGG_INCUBATOR", {}).get("result", 0) if code == 1: - logger.log('[x] Now incubating a ' + str(egg["km"]) + "km egg", 'green') + self.emit_event( + 'incubate', + formatted='Incubating a {distance_in_km} egg.', + data={ + 'distance_in_km': str(egg['km']) + } + ) egg["used"] = True incubator["used"] = True break elif code == 5 or code == 7: - if self.bot.config.debug: - logger.log('[x] Incubator already in use') + self.emit_event( + 'incubator_already_used', + level='debug', + formatted='Incubator in use.', + ) incubator["used"] = True break elif code == 6: - if self.bot.config.debug: - logger.log('[x] Egg already incubating') + self.emit_event( + 'egg_already_incubating', + level='debug', + formatted='Egg already incubating', + ) egg["used"] = True def _check_inventory(self, lookup_ids=[]): @@ -152,16 +176,32 @@ def _hatch_eggs(self): except: pokemon_data = [{"name":"error","cp":"error","iv":"error"}] if not pokemon_ids or pokemon_data[0]['name'] == "error": - logger.log("[!] Eggs hatched, but we had trouble with the response. Please check your inventory to find your new pokemon!",'red') + self.emit_event( + 'egg_hatched', + data={ + 'pokemon': 'error', + 'cp': 'error', + 'iv': 'error', + 'exp': 'error', + 'stardust': 'error', + 'candy': 'error', + } + ) return - logger.log("-"*30, log_color) - logger.log("[!] {} eggs hatched! Received:".format(len(pokemon_data)), log_color) for i in range(len(pokemon_data)): - logger.log("-"*30,log_color) - logger.log("[!] Pokemon: {}".format(pokemon_data[i]['name']), log_color) - logger.log("[!] CP: {}".format(pokemon_data[i].get('cp',0)), log_color) - logger.log("[!] IV: {} ({:.2f})".format("/".join(map(str, pokemon_data[i]['iv'])),(sum(pokemon_data[i]['iv'])/self.max_iv)), log_color) - logger.log("[!] XP: {}".format(xp[i]), log_color) - logger.log("[!] Stardust: {}".format(stardust[i]), log_color) - logger.log("[!] Candy: {}".format(candy[i]), log_color) - logger.log("-"*30, log_color) + msg = "Egg hatched with a {pokemon} (CP {cp} - IV {iv}), {exp} exp, {stardust} stardust and {candy} candies." + self.emit_event( + 'egg_hatched', + formatted=msg, + data={ + 'pokemon': pokemon_data[i]['name'], + 'cp': pokemon_data[i]['cp'], + 'iv': "{} {}".format( + "/".join(map(str, pokemon_data[i]['iv'])), + sum(pokemon_data[i]['iv'])/self.max_iv + ), + 'exp': xp[i], + 'stardust': stardust[i], + 'candy': candy[i], + } + ) diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 2313cc504d..f43d1641e6 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -1,4 +1,6 @@ -from pokemongo_bot import logger +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + from pokemongo_bot.constants import Constants from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult @@ -16,7 +18,10 @@ def initialize(self): def should_run(self): has_space_for_loot = self.bot.has_space_for_loot() if not has_space_for_loot: - logger.log("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.", 'yellow') + 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." + ) return has_space_for_loot or self.bot.softban def is_attracted(self): @@ -35,7 +40,7 @@ def work(self): lng = nearest_fort['longitude'] fortID = nearest_fort['id'] details = fort_details(self.bot, fortID, lat, lng) - fort_name = details.get('name', 'Unknown').encode('utf8', 'replace') + fort_name = details.get('name', 'Unknown') unit = self.bot.config.distance_unit # Unit to use when printing formatted distance @@ -47,12 +52,24 @@ def work(self): ) if dist > Constants.MAX_DISTANCE_FORT_IS_REACHABLE: + fort_event_data = { + 'fort_name': u"{}".format(fort_name), + 'distance': format_dist(dist, unit), + } + if self.is_attracted() > 0: - add_str = ' (attraction of lure {})'.format(format_dist(self.lure_distance, unit)) + fort_event_data.update(lure_distance=format_dist(self.lure_distance, unit)) + self.emit_event( + 'moving_to_lured_fort', + formatted="Moving towards pokestop {fort_name} - {distance} (attraction of lure {lure_distance})", + data=fort_event_data + ) else: - add_str = '' - - logger.log('Moving towards fort {}, {} left{}'.format(fort_name, format_dist(dist, unit), add_str)) + self.emit_event( + 'moving_to_fort', + formatted="Moving towards pokestop {fort_name} - {distance}", + data=fort_event_data + ) step_walker = StepWalker( self.bot, @@ -64,7 +81,10 @@ def work(self): if not step_walker.step(): return WorkerResult.RUNNING - logger.log('Arrived at pokestop.') + self.emit_event( + 'arrived_at_fort', + formatted='Arrived at fort.' + ) return WorkerResult.SUCCESS def _get_nearest_fort_on_lure_way(self, forts): diff --git a/pokemongo_bot/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py index 2c61d87a9f..29df15ae4a 100644 --- a/pokemongo_bot/cell_workers/nickname_pokemon.py +++ b/pokemongo_bot/cell_workers/nickname_pokemon.py @@ -1,4 +1,3 @@ -from pokemongo_bot import logger from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.cell_workers.base_task import BaseTask @@ -35,7 +34,10 @@ def _nickname_pokemon(self,pokemon): new_name = "" instance_id = pokemon.get('id',0) if not instance_id: - logger.log("Pokemon instance id returned 0. Can't rename.",'red') + self.emit_event( + 'api_error', + formatted='Failed to get pokemon name, will not rename.' + ) return id = pokemon.get('pokemon_id',0)-1 name = self.bot.pokemon_list[id]['Name'] @@ -59,29 +61,41 @@ def _nickname_pokemon(self,pokemon): iv_sum=iv_sum, iv_pct=iv_pct)[:12] except KeyError as bad_key: - logger.log("Unable to nickname {} due to bad template ({})".format(name,bad_key),log_color) - if pokemon.get('nickname', "") == new_name: + self.emit_event( + 'config_error', + formatted="Unable to nickname {} due to bad template ({})".format(name,bad_key) + ) + if pokemon.get('nickname', '') == new_name: return response = self.bot.api.nickname_pokemon(pokemon_id=instance_id,nickname=new_name) sleep(1.2) try: result = reduce(dict.__getitem__, ["responses", "NICKNAME_POKEMON"], response) except KeyError: - logger.log("Attempt to nickname received bad response from server.",log_color) - if self.bot.config.debug: - logger.log(response,log_color) - return + self.emit_event( + 'api_error', + formatted='Attempt to nickname received bad response from server.' + ) result = result['result'] - if new_name == "": - new_name = name - output = { - 0: 'Nickname unset', - 1: 'Nickname set successfully! {} is now {}'.format(name,new_name), - 2: 'Invalid nickname! ({})'.format(new_name), - 3: 'Pokemon not found.', - 4: 'Pokemon is egg' - }[result] - if result==1: - log_color='green' + new_name = new_name or name + if result == 0: + self.emit_event( + 'unset_pokemon_nickname', + formatted="Pokemon nickname unset." + ) + elif result == 1: + self.emit_event( + 'rename_pokemon', + formatted="Pokemon {old_name} renamed to {current_name}", + data={ + 'old_name': name, + 'current_name': new_name + } + ) pokemon['nickname'] = new_name - logger.log(output,log_color) \ No newline at end of file + elif result == 2: + self.emit_event( + 'pokemon_nickname_invalid', + formatted="Nickname {nickname} is invalid", + data={'nickname': new_name} + ) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 3f610e7558..afa5578267 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- import time -from pokemongo_bot import logger from pokemongo_bot.human_behaviour import (normalized_reticle_size, sleep, spin_modifier) +from pokemongo_bot.cell_workers.base_task import BaseTask -class PokemonCatchWorker(object): +class PokemonCatchWorker(BaseTask): BAG_FULL = 'bag_full' NO_POKEBALLS = 'no_pokeballs' @@ -60,22 +60,33 @@ def work(self, response_dict=None): pokemon_potential = self.pokemon_potential(pokemon_data) pokemon_num = int(pokemon_data['pokemon_id']) - 1 pokemon_name = self.pokemon_list[int(pokemon_num)]['Name'] - logger.log('A Wild {} appeared! [CP {}] [Potential {}]'.format( - pokemon_name, cp, pokemon_potential), 'yellow') - logger.log('IV [Attack/Defense/Stamina] = [{}]'.format(iv_display)) + msg = 'A wild {pokemon} appeared! [CP {cp}] [Potential {iv}] [S/A/D {iv_display}]' + self.emit_event( + 'pokemon_appeared', + formatted=msg, + data={ + 'pokemon': pokemon_name, + 'cp': cp, + 'iv': pokemon_potential, + 'iv_display': iv_display, + } + ) + pokemon_data['name'] = pokemon_name # Simulate app sleep(3) if not self.should_capture_pokemon(pokemon_name, cp, pokemon_potential, response_dict): - # logger.log('[x] Rule prevents capture.') return False flag_VIP = False # @TODO, use the best ball in stock to catch VIP (Very Important Pokemon: Configurable) if self.check_vip_pokemon(pokemon_name, cp, pokemon_potential): - logger.log('[-] {} is a VIP Pokemon! [CP {}] [Potential {}] Nice! Try our best to catch it!'.format(pokemon_name, cp, pokemon_potential),'red') + self.emit_event( + 'vip_pokemon', + formatted='This is a VIP pokemon. Catch!!!' + ) flag_VIP=True items_stock = self.bot.current_inventory() @@ -89,7 +100,16 @@ def work(self, response_dict=None): if flag_VIP: if(berries_count>0 and catch_rate[pokeball-1] < 0.9): 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.emit_event( + 'pokemon_catch_rate', + level='debug', + formatted="Catch rate of {catch_rate} is low. Maybe will throw {berry_name} ({berry_count} left)", + data={ + 'catch_rate': success_percentage, + 'berry_name': self.item_list[str(berry_id)], + 'berry_count': berries_count + } + ) # Out of all pokeballs! Let's don't waste berry. if items_stock[1] == 0 and items_stock[2] == 0 and items_stock[3] == 0: break @@ -107,12 +127,29 @@ def work(self, response_dict=None): success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) berries_count = berries_count -1 berry_used = True - logger.log('Threw a berry! Catch Rate with normal Pokeball has increased to {}%'.format(success_percentage)) + self.emit_event( + 'threw_berry', + formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", + data={ + "berry_name": self.item_list[str(berry_id)], + "new_catch_rate": success_percentage + } + ) else: if response_dict['status_code'] is 1: - logger.log('Fail to use berry. Seem like you are softbanned.', 'red') + self.emit_event( + 'softban', + level='warning', + formatted='Failed to use berry. You may be softbanned.' + ) else: - logger.log('Fail to use berry. Status Code: {}'.format(response_dict['status_code']),'red') + self.emit_event( + 'threw_berry_failed', + formatted='Unknown response when throwing berry: {status_code}.', + data={ + 'status_code': response_dict['status_code'] + } + ) #use the best ball to catch current_type = pokeball @@ -130,7 +167,16 @@ def work(self, response_dict=None): break 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.emit_event( + 'pokemon_catch_rate', + level='debug', + formatted="Catch rate of {catch_rate} is low. Maybe will throw {berry_name} ({berry_count} left)", + data={ + 'catch_rate': success_percentage, + 'berry_name': self.item_list[str(berry_id)], + 'berry_count': berries_count-1 + } + ) response_dict = self.api.use_item_capture(item_id=berry_id, encounter_id=encounter_id, spawn_point_id=self.spawn_point_guid @@ -142,12 +188,29 @@ def work(self, response_dict=None): success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) berries_count = berries_count -1 berry_used = True - logger.log('Threw a berry! Catch Rate with normal Pokeball has increased to {}%'.format(success_percentage)) + self.emit_event( + 'threw_berry', + formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", + data={ + "berry_name": self.item_list[str(berry_id)], + "new_catch_rate": success_percentage + } + ) else: if response_dict['status_code'] is 1: - logger.log('Fail to use berry. Seem like you are softbanned.', 'red') + self.emit_event( + 'softban', + level='warning', + formatted='Failed to use berry. You may be softbanned.' + ) else: - logger.log('Fail to use berry. Status Code: {}'.format(response_dict['status_code']),'red') + self.emit_event( + 'threw_berry_failed', + formatted='Unknown response when throwing berry: {status_code}.', + data={ + 'status_code': response_dict['status_code'] + } + ) else: #We don't have many berry to waste, pick a good ball first. Save some berry for future VIP pokemon @@ -165,7 +228,16 @@ def work(self, response_dict=None): break 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.emit_event( + 'pokemon_catch_rate', + level='debug', + formatted="Catch rate of {catch_rate} is low. Throwing {berry_name} ({berry_count} left)", + data={ + 'catch_rate': success_percentage, + 'berry_name': self.item_list[str(berry_id)], + 'berry_count': berries_count-1 + } + ) response_dict = self.api.use_item_capture(item_id=berry_id, encounter_id=encounter_id, spawn_point_id=self.spawn_point_guid @@ -177,12 +249,29 @@ def work(self, response_dict=None): success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) berries_count = berries_count -1 berry_used = True - logger.log('Threw a berry! Catch Rate with normal Pokeball has increased to {}%'.format(success_percentage)) + self.emit_event( + 'threw_berry', + formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", + data={ + "berry_name": self.item_list[str(berry_id)], + "new_catch_rate": success_percentage + } + ) else: if response_dict['status_code'] is 1: - logger.log('Fail to use berry. Seem like you are softbanned.', 'red') + self.emit_event( + 'softban', + level='warning', + formatted='Failed to use berry. You may be softbanned.' + ) else: - logger.log('Fail to use berry. Status Code: {}'.format(response_dict['status_code']),'red') + self.emit_event( + 'threw_berry_failed', + formatted='Unknown response when throwing berry: {status_code}.', + data={ + 'status_code': response_dict['status_code'] + } + ) # Re-check if berry is used, find a ball for a good capture rate current_type=pokeball @@ -203,12 +292,15 @@ def work(self, response_dict=None): items_stock[pokeball] -= 1 success_percentage = '{0:.2f}'.format(catch_rate[pokeball - 1] * 100) - logger.log('Using {} (chance: {}%)... ({} left!)'.format( - self.item_list[str(pokeball)], - success_percentage, - items_stock[pokeball] - )) - + self.emit_event( + 'threw_pokeball', + formatted='Used {pokeball}, with chance {success_percentage} ({count_left} left)', + data={ + 'pokeball': self.item_list[str(pokeball)], + 'success_percentage': success_percentage, + 'count_left': items_stock[pokeball] + } + ) id_list1 = self.count_pokemon_inventory() reticle_size_parameter = normalized_reticle_size(self.config.catch_randomize_reticle_factor) @@ -231,26 +323,35 @@ def work(self, response_dict=None): status = response_dict['responses'][ 'CATCH_POKEMON']['status'] if status is 2: - logger.log( - '[-] Attempted to capture {} - failed.. trying again!'.format(pokemon_name), - 'red') + self.emit_event( + 'pokemon_fled', + formatted="{pokemon} fled.", + data={'pokemon': pokemon_name} + ) sleep(2) continue if status is 3: - logger.log( - 'Oh no! {} vanished! :('.format(pokemon_name), 'red') + self.emit_event( + 'pokemon_vanished', + formatted="{pokemon} vanished!", + data={'pokemon': pokemon_name} + ) if success_percentage == 100: self.softban = True if status is 1: self.bot.metrics.captured_pokemon(pokemon_name, cp, iv_display, pokemon_potential) - logger.log('Captured {}! [CP {}] [Potential {}] [{}] [+{} exp]'.format( - pokemon_name, - cp, - pokemon_potential, - iv_display, - sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) - ), 'blue') + self.emit_event( + 'pokemon_caught', + formatted='Captured {pokemon}! [CP {cp}] [Potential {iv}] [{iv_display}] [+{exp} exp]', + data={ + 'pokemon': pokemon_name, + 'cp': cp, + 'iv': pokemon_potential, + 'iv_display': iv_display, + 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) + } + ) self.bot.softban = False if (self.config.evolve_captured @@ -267,11 +368,17 @@ def work(self, response_dict=None): response_dict = self.api.evolve_pokemon(pokemon_id=pokemon_to_transfer[0]) status = response_dict['responses']['EVOLVE_POKEMON']['result'] if status == 1: - logger.log( - '{} has been evolved!'.format(pokemon_name), 'green') + self.emit_event( + 'pokemon_evolved', + formatted="{pokemon} evolved!", + data={'pokemon': pokemon_name} + ) else: - logger.log( - 'Failed to evolve {}!'.format(pokemon_name)) + self.emit_event( + 'pokemon_evolve_fail', + formatted="Failed to evolve {pokemon}!", + data={'pokemon': pokemon_name} + ) break time.sleep(5) @@ -346,15 +453,6 @@ def should_capture_pokemon(self, pokemon_name, cp, iv, response_dict): 'and': lambda x, y: x and y } - # logger.log( - # "Catch config for {}: CP {} {} IV {}".format( - # pokemon_name, - # catch_cp, - # cp_iv_logic, - # catch_iv - # ), 'yellow' - # ) - return logic_to_function[cp_iv_logic](*catch_results.values()) def _get_catch_config_for(self, pokemon): @@ -419,4 +517,3 @@ def check_vip_pokemon(self,pokemon, cp, iv): 'and': lambda x, y: x and y } return logic_to_function[cp_iv_logic](*catch_results.values()) - diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 022a711f1a..c28b2749b1 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -1,6 +1,5 @@ import json import os -from pokemongo_bot import logger from pokemongo_bot.cell_workers.base_task import BaseTask from pokemongo_bot.tree_config_builder import ConfigException @@ -37,11 +36,23 @@ def work(self): result = response_dict_recycle.get('responses', {}).get('RECYCLE_INVENTORY_ITEM', {}).get('result', 0) if result == 1: # Request success - message_template = "-- Discarded {}x {} (keeps only {} maximum) " - message = message_template.format(str(items_recycle_count), item_name, str(id_filter_keep)) - logger.log(message, 'green') + 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: - logger.log("-- Failed to discard " + item_name, 'red') + 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 diff --git a/pokemongo_bot/cell_workers/sleep_schedule.py b/pokemongo_bot/cell_workers/sleep_schedule.py index 221867f0e6..daaf0b8f1e 100644 --- a/pokemongo_bot/cell_workers/sleep_schedule.py +++ b/pokemongo_bot/cell_workers/sleep_schedule.py @@ -1,7 +1,6 @@ from datetime import datetime, timedelta from time import sleep from random import uniform -from pokemongo_bot import logger from pokemongo_bot.cell_workers.base_task import BaseTask @@ -63,7 +62,13 @@ def _process_config(self): def _schedule_next_sleep(self): self._next_sleep = self._get_next_sleep_schedule() self._next_duration = self._get_next_duration() - logger.log('SleepSchedule: next sleep at {}'.format(str(self._next_sleep)), color='green') + self.emit_event( + 'next_sleep', + formatted="Next sleep at {time}", + data={ + 'time': str(self._next_sleep) + } + ) def _get_next_sleep_schedule(self): now = datetime.now() + self.SCHEDULING_MARGIN @@ -87,9 +92,14 @@ def _get_random_offset(self, max_offset): def _sleep(self): sleep_to_go = self._next_duration - logger.log('It\'s time for sleep.') + self.emit_event( + 'bot_sleep', + formatted="Sleeping for {time_in_seconds}", + data={ + 'time_in_seconds': sleep_to_go + } + ) while sleep_to_go > 0: - logger.log('Sleeping for {} more seconds'.format(sleep_to_go), 'yellow') if sleep_to_go < self.LOG_INTERVAL_SECONDS: sleep(sleep_to_go) sleep_to_go = 0 diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index f34fc2b8ef..9572008241 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals import time from pgoapi.utilities import f2i -from pokemongo_bot import logger from pokemongo_bot.constants import Constants from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.worker_result import WorkerResult @@ -15,7 +15,10 @@ class SpinFort(BaseTask): def should_run(self): if not self.bot.has_space_for_loot(): - logger.log("Not spinning any forts as there aren't enough space. You might want to change your config to recycle more items if this message appears consistently.", 'yellow') + 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." + ) return False return True @@ -29,9 +32,7 @@ def work(self): lng = fort['longitude'] details = fort_details(self.bot, fort['id'], lat, lng) - fort_name = details.get('name', 'Unknown').encode('utf8', 'replace') - logger.log('Now at Pokestop: {0}'.format(fort_name), 'cyan') - logger.log('Spinning ...', 'cyan') + fort_name = details.get('name', 'Unknown') response_dict = self.bot.api.fort_search( fort_id=fort['id'], @@ -47,69 +48,86 @@ def work(self): spin_result = spin_details.get('result', -1) if spin_result == 1: self.bot.softban = False - logger.log("Loot: ", 'green') - experience_awarded = spin_details.get('experience_awarded', - False) - if experience_awarded: - logger.log(str(experience_awarded) + " xp", - 'green') - - items_awarded = spin_details.get('items_awarded', False) + experience_awarded = spin_details.get('experience_awarded', 0) + items_awarded = spin_details.get('items_awarded', {}) if items_awarded: self.bot.latest_inventory = None tmp_count_items = {} for item in items_awarded: item_id = item['item_id'] - if not item_id in tmp_count_items: - tmp_count_items[item_id] = item['item_count'] - else: - tmp_count_items[item_id] += item['item_count'] - - for item_id, item_count in tmp_count_items.iteritems(): item_name = self.bot.item_list[str(item_id)] - logger.log( - '- ' + str(item_count) + "x " + item_name + - " (Total: " + str(self.bot.item_inventory_count(item_id)) + ")", 'yellow' - ) + if not item_name in tmp_count_items: + tmp_count_items[item_name] = item['item_count'] + else: + tmp_count_items[item_name] += item['item_count'] + + if experience_awarded or items_awarded: + self.emit_event( + 'spun_pokestop', + formatted="Spun pokestop {pokestop}. Experience awarded: {exp}. Items awarded: {items}", + data={ + 'pokestop': fort_name, + 'exp': experience_awarded, + 'items': tmp_count_items + } + ) else: - logger.log("[#] Nothing found.", 'yellow') - + self.emit_event( + 'pokestop_empty', + formatted='Found nothing in pokestop {pokestop}.', + data={'pokestop': fort_name} + ) pokestop_cooldown = spin_details.get( 'cooldown_complete_timestamp_ms') self.bot.fort_timeouts.update({fort["id"]: pokestop_cooldown}) - if pokestop_cooldown: - seconds_since_epoch = time.time() - logger.log('PokeStop on cooldown. Time left: ' + str( - format_time((pokestop_cooldown / 1000) - - seconds_since_epoch))) - self.bot.recent_forts = self.bot.recent_forts[1:] + [fort['id']] elif spin_result == 2: - logger.log("[#] Pokestop out of range") + self.emit_event( + 'pokestop_out_of_range', + formatted="Pokestop {pokestop} out of range.", + data={'pokestop': fort_name} + ) elif spin_result == 3: pokestop_cooldown = spin_details.get( 'cooldown_complete_timestamp_ms') if pokestop_cooldown: self.bot.fort_timeouts.update({fort["id"]: pokestop_cooldown}) seconds_since_epoch = time.time() - logger.log('PokeStop on cooldown. Time left: ' + str( - format_time((pokestop_cooldown / 1000) - - seconds_since_epoch))) + minutes_left = format_time( + (pokestop_cooldown / 1000) - seconds_since_epoch + ) + self.emit_event( + 'pokestop_on_cooldown', + formatted="Pokestop {pokestop} on cooldown. Time left: {minutes_left}.", + data={'pokestop': fort_name, 'minutes_left': minutes_left} + ) elif spin_result == 4: - logger.log("Inventory is full", 'red') + self.emit_event( + 'inventory_full', + formatted="Inventory is full!" + ) else: - logger.log("Unknown spin result: " + str(spin_result), 'red') - + self.emit_event( + 'unknown_spin_result', + formatted="Unknown spint result {status_code}", + data={'status_code': str(spin_result)} + ) if 'chain_hack_sequence_number' in response_dict['responses'][ 'FORT_SEARCH']: time.sleep(2) return response_dict['responses']['FORT_SEARCH'][ 'chain_hack_sequence_number'] else: - logger.log('Possibly searching too often - taking a short rest :)', 'yellow') + self.emit_event( + 'pokestop_searching_too_often', + formatted="Possibly searching too often, take a rest." + ) if spin_result == 1 and not items_awarded and not experience_awarded and not pokestop_cooldown: self.bot.softban = True - logger.log('[!] Possibly got softban too...', 'red') + self.emit_event( + 'softban', + formatted='Probably got softban.' + ) else: self.bot.fort_timeouts[fort["id"]] = (time.time() + 300) * 1000 # Don't spin for 5m return 11 diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 374ffa6a87..70c5939c58 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -1,9 +1,9 @@ import json -from pokemongo_bot import logger from pokemongo_bot.human_behaviour import action_delay from pokemongo_bot.cell_workers.base_task import BaseTask + class TransferPokemon(BaseTask): def work(self): pokemon_groups = self._release_pokemon_get_groups() @@ -41,6 +41,17 @@ def work(self): all_pokemons.remove(pokemon) best_pokemons.append(pokemon) + if best_pokemons and all_pokemons: + self.emit_event( + 'keep_best_release', + formatted="Keeping best {amount} {pokemon}, based on {criteria}", + data={ + 'amount': len(best_pokemons), + 'pokemon': pokemon_name, + 'criteria': order_criteria + } + ) + transfer_pokemons = [pokemon for pokemon in all_pokemons if self.should_release_pokemon(pokemon_name, pokemon['cp'], @@ -48,16 +59,6 @@ def work(self): True)] if transfer_pokemons: - logger.log("Keep {} best {}, based on {}".format(len(best_pokemons), - pokemon_name, - order_criteria), "green") - for best_pokemon in best_pokemons: - logger.log("{} [CP {}] [Potential {}]".format(pokemon_name, - best_pokemon['cp'], - best_pokemon['iv']), 'green') - - logger.log("Transferring {} pokemon".format(len(transfer_pokemons)), "green") - for pokemon in transfer_pokemons: self.release_pokemon(pokemon_name, pokemon['cp'], pokemon['iv'], pokemon['pokemon_data']['id']) else: @@ -167,24 +168,32 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): } if logic_to_function[cp_iv_logic](*release_results.values()): - logger.log( - "Releasing {} with CP {} and IV {}. Matching release rule: CP < {} {} IV < {}. ".format( - pokemon_name, - cp, - iv, - release_cp, - cp_iv_logic.upper(), - release_iv - ), 'yellow' + self.emit_event( + 'future_pokemon_release', + formatted="Releasing {pokemon} (CP {cp}/IV {iv}) based on rule: CP < {below_cp} {cp_iv_logic} IV < {below_iv}", + data={ + 'pokemon': pokemon_name, + 'cp': cp, + 'iv': iv, + 'below_cp': release_cp, + 'cp_iv_logic': cp_iv_logic.upper(), + 'below_iv': release_iv + } ) return logic_to_function[cp_iv_logic](*release_results.values()) def release_pokemon(self, pokemon_name, cp, iv, pokemon_id): - logger.log('Exchanging {} [CP {}] [Potential {}] for candy!'.format(pokemon_name, - cp, - iv), 'green') response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon_id) + self.emit_event( + 'pokemon_release', + formatted='Exchanged {pokemon} [CP {cp}] [IV {iv}] for candy.', + data={ + 'pokemon': pokemon_name, + 'cp': cp, + 'iv': iv + } + ) action_delay(self.bot.config.action_wait_min, self.bot.config.action_wait_max) def _get_release_config_for(self, pokemon): @@ -216,7 +225,6 @@ def _validate_keep_best_config(self, pokemon_name): keep_best_iv = 0 if keep_best_cp < 0 or keep_best_iv < 0: - logger.log("Keep best can't be < 0. Ignore it.", "red") keep_best = False if keep_best_cp == 0 and keep_best_iv == 0: diff --git a/pokemongo_bot/event_handlers/logging_handler.py b/pokemongo_bot/event_handlers/logging_handler.py index c54f435e19..7ad5720f6a 100644 --- a/pokemongo_bot/event_handlers/logging_handler.py +++ b/pokemongo_bot/event_handlers/logging_handler.py @@ -1,13 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + import logging from pokemongo_bot.event_manager import EventHandler class LoggingHandler(EventHandler): + def handle_event(self, event, sender, level, formatted_msg, data): logger = logging.getLogger(type(sender).__name__) if formatted_msg: - message = formatted_msg + message = "[{}] {}".format(event, formatted_msg) else: message = '{}: {}'.format(event, str(data)) getattr(logger, level)(message) diff --git a/pokemongo_bot/event_handlers/socketio_handler.py b/pokemongo_bot/event_handlers/socketio_handler.py index 3c850ca4c4..05b095313a 100644 --- a/pokemongo_bot/event_handlers/socketio_handler.py +++ b/pokemongo_bot/event_handlers/socketio_handler.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + from socketIO_client import SocketIO from pokemongo_bot.event_manager import EventHandler @@ -6,19 +10,21 @@ class SocketIoHandler(EventHandler): - def __init__(self, url): - super(EventHandler, self).__init__() + def __init__(self, bot, url): + self.bot = bot self.host, port_str = url.split(':') self.port = int(port_str) + self.sio = SocketIO(self.host, self.port) def handle_event(self, event, sender, level, msg, data): if msg: - date['msg'] = msg - with SocketIO(self.host, self.port) as sio: - sio.emit( - 'bot:broadcast', - { - 'event': event, - 'data': data, - } - ) + data['msg'] = msg + + self.sio.emit( + 'bot:broadcast', + { + 'event': event, + 'account': self.bot.config.username, + 'data': data + } + ) diff --git a/pokemongo_bot/event_manager.py b/pokemongo_bot/event_manager.py index 9af63690e6..3773ec8a9e 100644 --- a/pokemongo_bot/event_manager.py +++ b/pokemongo_bot/event_manager.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + class EventNotRegisteredException(Exception): pass @@ -7,6 +11,7 @@ class EventMalformedException(Exception): class EventHandler(object): + def __init__(self): pass @@ -15,17 +20,27 @@ def handle_event(self, event, kwargs): class EventManager(object): + def __init__(self, *handlers): self._registered_events = dict() self._handlers = handlers or [] + def event_report(self): + for event, parameters in self._registered_events.iteritems(): + print '-'*80 + print 'Event: {}'.format(event) + if parameters: + print 'Parameters:' + for parameter in parameters: + print '* {}'.format(parameter) + def add_handler(self, event_handler): self._handlers.append(event_handler) - def register_event(self, name, parameters=None): + def register_event(self, name, parameters=[]): self._registered_events[name] = parameters - def emit(self, event, sender=None, level='info', formatted=None, data={}): + def emit(self, event, sender=None, level='info', formatted='', data={}): if not sender: raise ArgumentError('Event needs a sender!') @@ -38,9 +53,10 @@ def emit(self, event, sender=None, level='info', formatted=None, data={}): # verify params match event parameters = self._registered_events[event] - for k, v in data.iteritems(): - if k not in parameters: - raise EventMalformedException("Event %s does not require parameter %s" % (event, k)) + if parameters: + for k, v in data.iteritems(): + if k not in parameters: + raise EventMalformedException("Event %s does not require parameter %s" % (event, k)) formatted_msg = formatted.format(**data) diff --git a/pokemongo_bot/health_record/bot_event.py b/pokemongo_bot/health_record/bot_event.py index dad0b49c42..3160593251 100644 --- a/pokemongo_bot/health_record/bot_event.py +++ b/pokemongo_bot/health_record/bot_event.py @@ -1,25 +1,26 @@ # -*- coding: utf-8 -*- +import logging + from time import sleep from UniversalAnalytics import Tracker -from pokemongo_bot import logger - class BotEvent(object): def __init__(self, bot): self.bot = bot + self.logger = logging.getLogger(__name__) # UniversalAnalytics can be reviewed here: # https://github.com/analytics-pros/universal-analytics-python # For central TensorFlow training, forbiden any personally information # report to server # Review Very Carefully for the following line, forbiden ID changed PR: if bot.config.health_record: - logger.log( + logger.info( 'Send anonymous bot health report to server, ' 'it can be disabled by config \"health_record\":false in config file', 'red' ) - logger.log('Wait for 2 seconds ', 'red') + logger.info('Wait for 2 seconds ', 'red') sleep(3) self.tracker = Tracker.create('UA-81469507-1', use_post=True) diff --git a/pokemongo_bot/logger.py b/pokemongo_bot/logger.py index 36c99c92f5..ebb59b7599 100644 --- a/pokemongo_bot/logger.py +++ b/pokemongo_bot/logger.py @@ -1,36 +1,20 @@ -from __future__ import unicode_literals -import time +import warnings +import logging -try: - import lcd - lcd = lcd.lcd() - # Change this to your i2c address - lcd.set_addr(0x23) -except Exception: - lcd = False +def log(msg, color=None): + warnings.simplefilter('always', DeprecationWarning) + message = ( + "Using logger.log is deprecated and will be removed soon. " + "We recommend that you try to log as little as possible " + "and use the event system to send important messages " + "(they become logs and websocket messages) automatically). " + "If you don't think your message should go to the websocket " + "server but it's really necessary, use the self.logger variable " + "inside any class inheriting from BaseTask to log." -def log(string, color='white'): - color_hex = { - 'red': '91m', - 'green': '92m', - 'yellow': '93m', - 'blue': '94m', - 'cyan': '96m' - } - if color not in color_hex: - print('[{time}] {string}'.format( - time=time.strftime("%H:%M:%S"), - string=string.decode('utf-8') - )) - else: - print( - '[{time}] \033[{color} {string} \033[0m'.format( - time=time.strftime("%H:%M:%S"), - color=color_hex[color], - string=string.decode('utf-8') - ) - ) - if lcd: - if string: - lcd.message(string) + ) + + logger = logging.getLogger('generic') + logger.info(msg) + warnings.warn(message, DeprecationWarning) diff --git a/pokemongo_bot/socketio_server/app.py b/pokemongo_bot/socketio_server/app.py index 4aac36c202..09c237f910 100644 --- a/pokemongo_bot/socketio_server/app.py +++ b/pokemongo_bot/socketio_server/app.py @@ -3,32 +3,30 @@ import socketio from flask import Flask -from pokemongo_bot.event_handlers import LoggingHandler -from pokemongo_bot.event_manager import EventManager sio = socketio.Server(async_mode='eventlet', logging=logging.NullHandler) app = Flask(__name__) -event_manager = EventManager() -event_manager.add_handler(LoggingHandler()) -event_manager.register_event( - "websocket_client_connected", -) - # client asks for data @sio.on('remote:send_request') def remote_control(sid, command): - sio.emit('bot:process_request', data=command) + if not 'account' in command: + return False + bot_name = command.pop('account') + event = 'bot:process_request:{}'.format(bot_name) + sio.emit(event, data=command) # sending bot response to client @sio.on('bot:send_reply') def request_reply(sid, response): - sio.emit(response['command'], response['response']) + event = response.pop('command') + account = response.pop('account') + event = "{}:{}".format(event, account) + sio.emit(event, response) @sio.on('bot:broadcast') def bot_broadcast(sid, env): - sio.emit(env['event'], data=env['data']) - -@sio.on('disconnect') -def disconnect(sid): - print('disconnect ', sid) + event = env.pop('event') + account = env.pop('account') + event_name = "{}:{}".format(event, account) + sio.emit(event_name, data=env['data']) diff --git a/pokemongo_bot/test/follow_cluster_test.py b/pokemongo_bot/test/follow_cluster_test.py index b44cbfc16a..4820a7558a 100644 --- a/pokemongo_bot/test/follow_cluster_test.py +++ b/pokemongo_bot/test/follow_cluster_test.py @@ -15,7 +15,11 @@ def testWorkAway(self, mock_pokemongo_bot): mock_pokemongo_bot.config.walk = 4.16 mock_pokemongo_bot.get_forts.return_value = ex_forts follow_cluster = FollowCluster(mock_pokemongo_bot, config) - assert follow_cluster.work() == [37.39718375014263, -5.9932912500000013] + + expected = (37.397183750142624, -5.9932912500000013) + result = follow_cluster.work() + self.assertAlmostEqual(expected[0], result[0], delta=0.000000000010000) + self.assertAlmostEqual(expected[1], result[1], delta=0.000000000010000) assert follow_cluster.is_at_destination == False assert follow_cluster.announced == False @@ -29,6 +33,10 @@ def testWorkArrived(self, mock_pokemongo_bot): mock_pokemongo_bot.config.walk = 4.16 mock_pokemongo_bot.get_forts.return_value = ex_forts follow_cluster = FollowCluster(mock_pokemongo_bot, config) - assert follow_cluster.work() == [37.39718375014263, -5.9932912500000013] + + expected = (37.397183750142624, -5.9932912500000013) + result = follow_cluster.work() + self.assertAlmostEqual(expected[0], result[0], delta=0.000000000010000) + self.assertAlmostEqual(expected[1], result[1], delta=0.000000000010000) assert follow_cluster.is_at_destination == True assert follow_cluster.announced == False diff --git a/pokemongo_bot/walkers/polyline_walker.py b/pokemongo_bot/walkers/polyline_walker.py index 001b7c77cf..687371e866 100644 --- a/pokemongo_bot/walkers/polyline_walker.py +++ b/pokemongo_bot/walkers/polyline_walker.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from pokemongo_bot import logger from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.step_walker import StepWalker from polyline_generator import Polyline @@ -12,7 +11,13 @@ def __init__(self, bot, speed, dest_lat, dest_lng): super(PolylineWalker, self).__init__(bot, speed, dest_lat, dest_lng) self.polyline_walker = Polyline((self.api._position_lat, self.api._position_lng), (self.destLat, self.destLng), self.speed) - logger.log('[#] {}'.format(self.polyline_walker.URL)) + self.bot.event_manager.emit( + 'polyline_request', + sender=self, + level='info', + formatted="{url}", + data={'url': self.polyline_walker.URL} + ) def step(self): cLat, cLng = self.api._position_lat, self.api._position_lng diff --git a/pokemongo_bot/websocket_remote_control.py b/pokemongo_bot/websocket_remote_control.py index cd8e04e0fe..c4e15362b6 100644 --- a/pokemongo_bot/websocket_remote_control.py +++ b/pokemongo_bot/websocket_remote_control.py @@ -10,7 +10,10 @@ def __init__(self, bot): self.host, port_str = self.bot.config.websocket_server_url.split(':') self.port = int(port_str) self.sio = SocketIO(self.host, self.port) - self.sio.on('bot:process_request', self.on_remote_command) + self.sio.on( + 'bot:process_request:{}'.format(self.bot.config.username), + self.on_remote_command + ) self.thread = threading.Thread(target=self.process_messages) def start(self): @@ -28,7 +31,8 @@ def on_remote_command(self, command): 'bot:send_reply', { 'response': '', - 'command': 'command_not_found' + 'command': 'command_not_found', + 'account': self.bot.config.username } ) return @@ -42,7 +46,8 @@ def get_player_info(self): self.sio.emit( 'bot:send_reply', { - 'response': player_info, - 'command': 'get_player_info' + 'result': player_info, + 'command': 'get_player_info', + 'account': self.bot.config.username } ) diff --git a/tests/__init__.py b/tests/__init__.py index 5318e2d482..c02aed60fc 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,7 @@ # __init__.py from mock import MagicMock +from pokemongo_bot.event_manager import EventManager from pokemongo_bot.api_wrapper import ApiWrapper, ApiRequest from pokemongo_bot import PokemonGoBot @@ -13,8 +14,10 @@ def create_request(self, return_value='mock return'): class FakeBot(PokemonGoBot): def __init__(self): - self.config = MagicMock() + self.config = MagicMock(websocket_server_url=False, show_events=False) self.api = FakeApi() + self.event_manager = EventManager() + self._setup_event_system() def updateConfig(self, conf): self.config.__dict__.update(conf) diff --git a/ws_server.py b/ws_server.py new file mode 100755 index 0000000000..85d6bb3a01 --- /dev/null +++ b/ws_server.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# -*- coding: utf-8 - + +import argparse + +from pokemongo_bot.socketio_server.runner import SocketIoRunner + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument( + "--host", + help="Host for the websocket", + type=str, + default='localhost' + ) + parser.add_argument( + "--port", + help="Port for the websocket", + type=int, + default=4000 + ) + config = parser.parse_known_args()[0] + + s = SocketIoRunner("{}:{}".format(config.host, config.port)) + s._start_listening_blocking() From 4f48e0a896dfb1e00e502120f7d5fc2dbf2f5b76 Mon Sep 17 00:00:00 2001 From: xnning Date: Thu, 4 Aug 2016 07:37:37 +0800 Subject: [PATCH 020/202] correct parsing evolve_all (#2455) * correct parsing evolve_all Previously, ``` "evolve_all": "Pidgey, Caterpie, Weedle", ``` would only evolve Pidgey. This PR fix that. * fix parsing evolve_captured --- pokecli.py | 2 +- pokemongo_bot/cell_workers/evolve_pokemon.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pokecli.py b/pokecli.py index 94f8b01ee2..730af6c2ce 100755 --- a/pokecli.py +++ b/pokecli.py @@ -440,7 +440,7 @@ def task_configuration_error(flag_name): raise if config.evolve_captured and isinstance(config.evolve_captured, str): - config.evolve_captured = [str(pokemon_name) for pokemon_name in config.evolve_captured.split(',')] + config.evolve_captured = [str(pokemon_name).strip() for pokemon_name in config.evolve_captured.split(',')] fix_nested_config(config) return config diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 91c9c807cf..911d6a1f67 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -18,7 +18,7 @@ def initialize(self): def _validate_config(self): if isinstance(self.evolve_all, basestring): - self.evolve_all = [str(pokemon_name) for pokemon_name in self.evolve_all.split(',')] + self.evolve_all = [str(pokemon_name).strip() for pokemon_name in self.evolve_all.split(',')] def work(self): if not self._should_run(): From 053e0a4213a9f8c01d356187aded35f5f08c5e39 Mon Sep 17 00:00:00 2001 From: theVDude Date: Wed, 3 Aug 2016 19:38:40 -0400 Subject: [PATCH 021/202] Remove max_steps from examples and set EvolveAll to EvolvePokemon (#2430) --- configs/config.json.cluster.example | 1 - configs/config.json.map.example | 3 +-- configs/config.json.path.example | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index 5613061a84..8d0d8f854f 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -62,7 +62,6 @@ } } ], - "max_steps": 5, "forts": { "avoid_circles": true, "max_circle_size": 50 diff --git a/configs/config.json.map.example b/configs/config.json.map.example index 6436b95905..e665d4c6da 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -21,7 +21,7 @@ "type": "TransferPokemon" }, { - "type": "EvolveAll", + "type": "EvolvePokemon", "config": { "evolve_all": "NONE", "evolve_cp_min": 300, @@ -300,7 +300,6 @@ } ], "map_object_cache_time": 5, - "max_steps": 5, "forts": { "avoid_circles": true, "max_circle_size": 50 diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 38baa9f1f0..afd1e3afeb 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -63,7 +63,6 @@ } ], "map_object_cache_time": 5, - "max_steps": 5, "forts": { "avoid_circles": true, "max_circle_size": 50 From 046658444b78447b349ff137629f5da9b8a93922 Mon Sep 17 00:00:00 2001 From: Eli White Date: Wed, 3 Aug 2016 22:30:41 -0700 Subject: [PATCH 022/202] Adding Raven to send exception reports to Sentry (#2514) * Adding Raven to send exception reports to Sentry * Removing test exception * Removing incompatible python3 analytics library * Using logger.log * Using the correct logger --- pokecli.py | 4 ++ pokemongo_bot/health_record/bot_event.py | 76 ++++++++++++++++-------- requirements.txt | 2 +- 3 files changed, 56 insertions(+), 26 deletions(-) diff --git a/pokecli.py b/pokecli.py index 730af6c2ce..85ce33ecc1 100755 --- a/pokecli.py +++ b/pokecli.py @@ -39,6 +39,7 @@ from geopy.exc import GeocoderQuotaExceeded from pokemongo_bot import PokemonGoBot, TreeConfigBuilder +from pokemongo_bot.health_record import BotEvent if sys.version_info >= (2, 7, 9): ssl._create_default_https_context = ssl._create_unverified_context @@ -58,7 +59,10 @@ def main(): config = init_config() if not config: return + logger.info('Configuration initialized') + health_record = BotEvent(config) + health_record.login_success() finished = False diff --git a/pokemongo_bot/health_record/bot_event.py b/pokemongo_bot/health_record/bot_event.py index 3160593251..986b5f3c70 100644 --- a/pokemongo_bot/health_record/bot_event.py +++ b/pokemongo_bot/health_record/bot_event.py @@ -1,42 +1,68 @@ # -*- coding: utf-8 -*- -import logging - from time import sleep -from UniversalAnalytics import Tracker - +import logging +from raven import Client +import raven +import os +import uuid +import requests class BotEvent(object): - def __init__(self, bot): - self.bot = bot + def __init__(self, config): + self.config = config self.logger = logging.getLogger(__name__) # UniversalAnalytics can be reviewed here: # https://github.com/analytics-pros/universal-analytics-python - # For central TensorFlow training, forbiden any personally information - # report to server - # Review Very Carefully for the following line, forbiden ID changed PR: - if bot.config.health_record: - logger.info( - 'Send anonymous bot health report to server, ' - 'it can be disabled by config \"health_record\":false in config file', 'red' + if self.config.health_record: + self.logger.info('Health check is enabled. For more logrmation:') + self.logger.info('https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev#analytics') + self.client = Client( + dsn='https://8abac56480f34b998813d831de262514:196ae1d8dced41099f8253ea2c8fe8e6@app.getsentry.com/90254', + name='PokemonGof-Bot', + processors = ( + 'raven.processors.SanitizePasswordsProcessor', + 'raven.processors.RemoveStackLocalsProcessor' + ), + install_logging_hook = False, + hook_libraries = (), + enable_breadcrumbs = False, + logging = False, + context = {} ) - logger.info('Wait for 2 seconds ', 'red') - sleep(3) - self.tracker = Tracker.create('UA-81469507-1', use_post=True) - # No RAW send function to be added here, to keep everything clean + def capture_error(self): + if self.config.health_record: + self.client.captureException() + def login_success(self): - if self.bot.config.health_record: - self.tracker.send('pageview', '/loggedin', title='succ') + if self.config.health_record: + track_url('/loggedin') def login_failed(self): - if self.bot.config.health_record: - self.tracker.send('pageview', '/login', title='fail') + if self.config.health_record: + track_url('/login') def login_retry(self): - if self.bot.config.health_record: - self.tracker.send('pageview', '/relogin', title='relogin') + if self.config.health_record: + track_url('/relogin') def logout(self): - if self.bot.config.health_record: - self.tracker.send('pageview', '/logout', title='logout') + if self.config.health_record: + track_url('/logout') + + +def track_url(path): + data = { + 'v': '1', + 'tid': 'UA-81469507-1', + 'aip': '1', # Anonymize IPs + 'cid': uuid.uuid4(), + 't': 'pageview', + 'dp': path + } + + response = requests.post( + 'http://www.google-analytics.com/collect', data=data) + + response.raise_for_status() diff --git a/requirements.txt b/requirements.txt index 08f9407c81..d8b5ce5c9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ python-socketio==1.4.2 flask==0.11.1 socketIO_client==0.7.0 eventlet==0.19.0 -universal-analytics-python==0.2.4 gpxpy==1.1.1 mock==2.0.0 timeout-decorator==0.3.2 +raven==5.23.0 From 69fb64f2bf7c12e28c2bb6d2b636c6af55822448 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Thu, 4 Aug 2016 16:38:21 +0200 Subject: [PATCH 023/202] changing license from MIT to GPLv3 --- LICENSE | 676 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 671 insertions(+), 5 deletions(-) diff --git a/LICENSE b/LICENSE index d71a77b230..9cecc1d466 100644 --- a/LICENSE +++ b/LICENSE @@ -1,8 +1,674 @@ -The MIT License (MIT) -Copyright (c) 2016 PokemonGoF Team + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + Preamble -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. From 1d7f6b7e91e186dc0ba34d4a9fa999fc343a8665 Mon Sep 17 00:00:00 2001 From: Alex Ratford Date: Thu, 4 Aug 2016 18:53:59 +0100 Subject: [PATCH 024/202] Address API changes in README.md (#2590) added information about the API changes and where to find information. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 1a8af52065..acdbc430f8 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ # PokemonGo-Bot The Pokemon Go Bot, baking with community. +## Niantic Changes +Niantic have changed API responses, meaning that this bot and anything accessing the API though POGOProtos is currently broken. A number of developers from /r/pokemondev are working to address this and come up with a fix for this issue, find the [current status here](https://www.reddit.com/r/pokemongodev/comments/4w1cvr/pokemongo_current_api_status/) ## Help Needed on [Desktop Version](https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) ## Help with the project [Dev Bot](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Develop-PokemonGo-Bot) ## Project Chat @@ -163,6 +165,7 @@ Research Website [Nice Tool](https://thesilphroad.com/research) * th3w4y * Leaklessgfy * GregTampa + * AlexRatman ------- ## Credits From cc528df77869a0b8a6410379a822e368ec87cfb7 Mon Sep 17 00:00:00 2001 From: mjmadsen Date: Thu, 4 Aug 2016 13:51:21 -0500 Subject: [PATCH 025/202] Updated README.md to state bot status (#2586) * Added bot broken message at top of page. * Changed REAME.md to mirror #2590 * Reordered --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 281c4d2fc8..f99c5a837b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ + # PokemonGo-Bot PokemonGo bot is a project created by the [PokemonGoF](https://github.com/PokemonGoF) team. The project is currently setup in two main branches. `dev` and `master`. +## Niantic Changes + Niantic have changed API responses, meaning that this bot and anything accessing the API though POGOProtos is currently broken. A number of developers from /r/pokemondev are working to address this and come up with a fix for this issue, find the [current status here](https://www.reddit.com/r/pokemongodev/comments/4w1cvr/pokemongo_current_api_status/) + We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) ## Table of Contents From 90e5628c401cb3145cbb1a7008b9ec89c800af4c Mon Sep 17 00:00:00 2001 From: Eli White Date: Thu, 4 Aug 2016 21:53:18 -0700 Subject: [PATCH 026/202] Handling KeyboardInterrupt and some other exceptions (#2599) --- pokecli.py | 145 +++++++++++++++++++++++++++-------------------------- 1 file changed, 74 insertions(+), 71 deletions(-) diff --git a/pokecli.py b/pokecli.py index 85ce33ecc1..3d1b3736e7 100755 --- a/pokecli.py +++ b/pokecli.py @@ -51,79 +51,82 @@ logger.setLevel(logging.INFO) def main(): - - logger.info('PokemonGO Bot v1.0') - sys.stdout = codecs.getwriter('utf8')(sys.stdout) - sys.stderr = codecs.getwriter('utf8')(sys.stderr) - - config = init_config() - if not config: - return - - logger.info('Configuration initialized') - health_record = BotEvent(config) - health_record.login_success() - - finished = False - - while not finished: - try: - bot = PokemonGoBot(config) - bot.start() - tree = TreeConfigBuilder(bot, config.raw_tasks).build() - bot.workers = tree - bot.metrics.capture_stats() - - bot.event_manager.emit( - 'bot_start', - sender=bot, - level='info', - formatted='Starting bot...' - ) - - while True: - bot.tick() - - except KeyboardInterrupt: - bot.event_manager.emit( - 'bot_exit', - sender=bot, - level='info', - formatted='Exiting bot.' - ) - finished = True + try: + logger.info('PokemonGO Bot v1.0') + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + sys.stderr = codecs.getwriter('utf8')(sys.stderr) + + config = init_config() + if not config: + return + + logger.info('Configuration initialized') + health_record = BotEvent(config) + health_record.login_success() + + finished = False + + while not finished: + try: + bot = PokemonGoBot(config) + bot.start() + tree = TreeConfigBuilder(bot, config.raw_tasks).build() + bot.workers = tree + bot.metrics.capture_stats() + + bot.event_manager.emit( + 'bot_start', + sender=bot, + level='info', + formatted='Starting bot...' + ) + + while True: + bot.tick() + + except KeyboardInterrupt: + bot.event_manager.emit( + 'bot_exit', + sender=bot, + level='info', + formatted='Exiting bot.' + ) + finished = True + report_summary(bot) + + except NotLoggedInException: + wait_time = config.reconnecting_timeout * 60 + bot.event_manager.emit( + 'api_error', + sender=bot, + level='info', + formmated='Log logged in, reconnecting in {:s}'.format(wait_time) + ) + time.sleep(wait_time) + except ServerBusyOrOfflineException: + bot.event_manager.emit( + 'api_error', + sender=bot, + level='info', + formatted='Server busy or offline' + ) + except ServerSideRequestThrottlingException: + bot.event_manager.emit( + 'api_error', + sender=bot, + level='info', + formatted='Server is throttling, reconnecting in 30 seconds' + ) + time.sleep(30) + + except GeocoderQuotaExceeded: + raise Exception("Google Maps API key over requests limit.") + except Exception as e: + # always report session summary and then raise exception + if bot: report_summary(bot) - except NotLoggedInException: - wait_time = config.reconnecting_timeout * 60 - bot.event_manager.emit( - 'api_error', - sender=bot, - level='info', - formmated='Log logged in, reconnecting in {:s}'.format(wait_time) - ) - time.sleep(wait_time) - except ServerBusyOrOfflineException: - bot.event_manager.emit( - 'api_error', - sender=bot, - level='info', - formatted='Server busy or offline' - ) - except ServerSideRequestThrottlingException: - bot.event_manager.emit( - 'api_error', - sender=bot, - level='info', - formatted='Server is throttling, reconnecting in 30 seconds' - ) - time.sleep(30) - except GeocoderQuotaExceeded: - raise "Google Maps API key over requests limit." - except Exception as e: - # always report session summary and then raise exception - report_summary(bot) - raise e + raise e def report_summary(bot): if bot.metrics.start_time is None: From fe2eb5a9cbb68cd47d9d4f6c209595d4d19ff7ca Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sat, 6 Aug 2016 11:58:30 -0700 Subject: [PATCH 027/202] Update README.md (#2625) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index acdbc430f8..d5b44ae8a1 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ # PokemonGo-Bot The Pokemon Go Bot, baking with community. -## Niantic Changes +## Niantic Changes, currently there's big progress, we are waiting for the API update pkmngodev/Unknown6. + Niantic have changed API responses, meaning that this bot and anything accessing the API though POGOProtos is currently broken. A number of developers from /r/pokemondev are working to address this and come up with a fix for this issue, find the [current status here](https://www.reddit.com/r/pokemongodev/comments/4w1cvr/pokemongo_current_api_status/) ## Help Needed on [Desktop Version](https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) ## Help with the project [Dev Bot](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Develop-PokemonGo-Bot) From e8218f65a34119ee764cf6eece17df36337e5bec Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sat, 6 Aug 2016 13:42:52 -0700 Subject: [PATCH 028/202] Dev merge to master (#2627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [BUGFIX] Catch Pokemon while walking to fort (#979) Resolves: #821 * Added IGNORED_FILES list to pylint-recursive.py (#923) * Fixed a bug where the bot crashed if 'catch' or 'release' wasn't specified in config.json * Added install.sh and run.sh * Added IGNORED_FILES list to pylint-recursive.py * Removed --item_filter argument from pokecli Removed due to 'complexity' of new item_filter, therefore it should only be in config.json * Fix the teleporting on web ui (#1074) * Update README.md (#1063) The plural of pokemon is pokemon. * Broken connection fix (#1071) * Broken connection fix * Typos fix * Forgot to load itemfilter after deleting args input * Update README with Docker instructions (#759) * Add metrics logging and output on close (#1059) * Add metrics logging and output on close Output looks like the following: [17:10:07] Exiting PokemonGo Bot [17:10:07] [17:10:07] Ran for 0:00:20 [17:10:07] Total XP Earned: 210 Averaging: 36534.47/h [17:10:07] Travelled 0.01km [17:10:07] Visited 0 stops [17:10:07] Encountered 1 pokemon, 1 caught, 0 released, 0 evolved, 0 never seen before [17:10:07] Threw 1 pokeball [17:10:07] Earned 100 Stardust [17:10:07] [17:10:07] Highest CP Pokemon: Nidoran M [CP: 75] [IV: 1/10/5] Potential: 0.36 [17:10:07] Most Perfect Pokemon: Nidoran M [CP: 75] [IV: 1/10/5] Potential: 0.36 * Added Metrics class to collect end of run stats Tried to come up with a reasonable division of labour for how to gather the information. Open to feedback! * Revert logging changes Didn’t mean to affect this message any more. * Merge cells together to avoid staying in one cell too long (#1061) * Merge cells together to avoid staying in one cell too long This should help mitigate the issue where the bot travels to a stop that is farther than a nearby one because the nearer one is in another cell. I also release control back to the make loop after catching any pokemon. * PR Feedback fixes + Add concatenation of nearby cells rather than override. ~ Actually call the SeenFortWorker rather than just reference it. * Don't make work a property I seem to have made it one at some point, somehow… Go PyCharm! * Add check to ensure there are available gyms * Refactor EvolveAll and InitialTransfer workers (#941) * Refactor EvolveAll and InitialTransfer workers * Fixing Item import * Fixed 'Pokemon will now be caught from lures' (#1072) * Set evolve speed in config (#1090) * added evolve_speed * updated README.md to include evolve_speed * when filter set to 0, it will failed (#1101) * fixing item_list not found (#1120) * Fix Location caching doesn't work (#1031) (#1100) * Update _get_catch_config in pokemon_catch_worker (#1124) It should return the setting given by "any" in the catch_config file, instead of return {} for a "unspecified" pokemon. * unhappy api parameter name (#1137) * Making the metrics be printed correctly at the end of the run (#1136) * REVERT #1072 * FIX REVERTED #1072 * Moving logic for catching the visible pokemon out into a new worker (#1142) * Dump cells to enable custom front-end functionalities (#1145) * Dump cells to enable custom front-end functionalities Fixed merge issues for #1019 It now dumps the cell list as a json into data/cells-$username.json, so that more front-ends can use this information (I'm working on a Kivy-based one). * Updated ignore file skip new cellfiles * Adding a WorkerResult and the MoveToFortWorker only takes a single step towards a fort (#1146) * RecycleItemWorker implemented (runs on every tick) (#1156) * RecycleItemWorker implemented (runs on every tick) * moved RecycleItemWorker to a better place * recycle item worker logging improved * simplify if in item_inventory_count * removing extra space * Moving these flags into the workers. Make them run on each tick (#1159) * Removing duplicated release logic from catching pokemon (#1160) * Updated readme, contributors and gitignore file (#1161) * moved most of readme stuff to wiki in order for a clean readme table * added myself to contributors at last * removed old files from gitignore file * More items info at start (#1167) * Update __init__.py * Update pokemon_catch_worker.py * Update CONTRIBUTORS.md * removing modules that cant be imported (#1165) * removing modules that cant be imported * Updated pylint check It wont exit incorrect if no error exists * Fixing logspam for transferring pokemon (#1164) * Removing unnecessary walking from the move to fort worker (#1166) * Removed logging (#1171) Should not logg when logging is done once it finds something to release * Removing logging wrapping cleaning up the pokemon bag (#1172) * Revert "Removing duplicated release logic from catching pokemon" (#1188) * Hotfix/walk to pokestop and refactor to walker (#1193) * some nice refactoring to StepWalker to avoid repetition * removing sleep in navigator (bot should only sleep after walk and in workers) * fixing walk to pokestop * removing personal debug log from step walker * removing empty line * added random_lat_long_delta again to StepWalker (my bad) * completely removed walking progress bar (log trimming is required) * better log color when exchanging pokemon (red) * less sleep after spinning pokestop * spiral navigator shouldn't reuse step walker instance * Reduce number of inventory calls (#1231) There’s way too many API calls for the inventory, so I added a coached response that’s used until someone invalidates it (like when catching/throwing and spinning stops) * Add missing inventory fetch (#1233) To recycle an item with the correct number, we should force fetch from the server. * API update fixes (i2f etc) and lured pokemon catching (#1163) * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * Fix PogoAPI to a recent commit * Fix PogoAPI to a recent commit * Fix PogoAPI to a recent commit * Fix PogoAPI to a recent commit * Fix PogoAPI to a recent commit * Added missing method * Various bugfixes * Various bugfixes * Various bugfixes * Various bugfixes * Merging with recent commits * Restored RecycleItemsWorker call * Merged with latest commit * Fixed arguments in method call * Only work on forts when there is space in the bag. Do not switch mode (#1237) * config parsing clean-ups (#1240) * Update the location & location_cache logic (#1217) Previously, the location_cache can't be used, even people set location_cache to TRUE. Everytime, it starts from the initial position. Now the bot check the initial position first. Then check if people set location_cache. If so, read it and update it, otherwise use the initial location. If there is no initial location and no location cache, print msg and exit. * optimize docker usage (#1257) * update Dockerfile; install missed python-protobuf package; add CMD as a way of defining default arguments for the ENTRYPOINT * remove CMD command, default config parameter is also set in pokecli.py * Only catch things that inherit from Exception. Try to prevent KeyboardError from being swallowed. (#1270) * seperate worker for transfering pokemons to professor (#1281) pulled out initial transfer and transfer on catched pokemon to seperate worker * Revert "Transfer poke worker" (#1284) * Added GA in the README.me on dev branch first. * Revert "Added GA in the README.me on dev branch first." This reverts commit 90a17c09971472f89243e8498dc5663292c8212c. * adding random delay between pokemon capture & transfer. #774 (#1225) * adding random delay between pokemon capture & transfer. #774 * improved fix for #774 by adding click_action_delay function * wait time minimum & maximum are passed in from the config now * updated readme & contributors files * updated how action wait times are loaded from config * optimize docker usage (#1257) * update Dockerfile; install missed python-protobuf package; add CMD as a way of defining default arguments for the ENTRYPOINT * remove CMD command, default config parameter is also set in pokecli.py * Removed erroneous line in human_behavior and corrected bad merge resolution * updated configs & changed function name to action_delay * Fixed seperate transfer bug in worker (#1286) * seperate worker for transfering pokemons to professor pulled out initial transfer and transfer on catched pokemon to seperate worker * Fixed bug where config parameters wasnt checked * Initial_transfer renamed to release_pokemons Also updated the configuration argument function to release pokemon runs on tick method which is called after everyt small step the bot takes * added back exception which was missing * Default conf value and fixed typo * fixed typo for pokemon and updated config * added default value if its missing from conf NOTE: transfer conditions are set by "release" parameters in config, however we keep the highest CP of each pokemon, just to be sure we don't empty the bag. * Capital leter * missed to rename on some places Also enabled loggning to easier see why a certain pokemon may not be transfered to professor for candy * Log format update * moved runtime error to evolved_capture statement workaround if anyone should get it... fixing it in future * Anonymous login succ/failed/relogin/logout info to GA. (#1312) * There's a big warning before bot login and 2s wait for exit if the health report is turned on. Anonymous login succ/failed/relogin/logout info without any user account info will be sent to GA. The function is not called to wait Signal system merge. * Fixed page view is not true. * Removed [x] since we ditched it. * Refactoring get_nearest_fort code * Update README.md * Replacing config.mode with capture_pokemon and spin_forts * Removed the GA. * Revert "Removed the GA." This reverts commit 58d7a67e813e4bdc3dd96defc04a7ed1ff76ffa2. * Allow to keep stronger pokemon (#1302) * Allow to keep stronger pokemon. It is woring both with CP and IV * Remove not needed code * Add example of keep_best_iv into configuration * Add delay before pokemon transfering * Resolve merge conflicts * Use config.release_pokemon to determine should we release pokemon or not * Fix a bug * Update config.json.example (#1340) Update config.json.example 4c46ad7e6b41f4d66400aee82be469cdbbb26f80 * Adding the new flags to the config.json example files (#1344) * Add Mr. Mime to config.json.pokemons.example (#1350) Add Mr. Mime to configs/config.json.pokemons.example * Refactoring code into a SpinNearestFortWorker (#1351) * Removing CatchVisiblePokemonWorker's argument of cell (#1352) * Iterate over each worker and treat them the same (#1356) * Fix merge conflicts that caused bugs (#1361) * Merging some unnecessary methods and renaming take_step on the bot to… (#1360) * Merging some unnecessary methods and renaming take_step on the bot to tick * Merging variable definition * Use a more human friendly time format (#1364) * fixed transfer worker api crash (#1369) * fixed transfer worker api crash try catch exception to prevent bot from crashing * better cach error method * prevent call __getitem__ on bool error (#1355) * prevent call __getitem__ on bool error * fix for pr * update sample config to solve bug (#1392) buggy: "catch_above_cp": 0, "catch_above_iv": 0.8, "logic": "and" replaced: "always_catch": true * Added worker for incubating eggs (#1404) * Added worker for incubating eggs * Added options to configuration * Bugfix * Ignoring used eggs and incubators * Using cached inventory instead of getting a fresh copy * Implement proper version of keep_best option (#1395) * Implement proper version of keep_best option * Fix order of pokemons * Change formatting * Display kept pokemons, to be sure that it is working ok * Don't print same info again and again * Remove other worker using * Get back spacing format * Catch transfer worker error (#1423) * fixed transfer worker api crash try catch exception to prevent bot from crashing * better cach error method * Fix for clash between CLI and JSON args (#1420) * makes spin forts and catch pokemon config in json not being overwritten by default cli args value if user dont provide cli args * huge fix to how cli and json parameters are loaded The CLI parameter parser now uses JSON-loaded parameters as first fallback to missing parameters. The second fallback to missing parameters are the default values previously used. This is the perfect handling for making CLI args override JSON configuration only for provided args. Non-provided args that are not found in are set to the default value we think most users are going to like. * overriding config from loaded JSON is not necessary here anymore * trying to fix the auth_service parameters * add mixing cli args fixes that were forgotten * fixing unicode load in location * refactoring parameter configuration to avoid mistakes * changed the order of functions in pokecli.py to follow some guidelines main function comes first all all its children below * small fix to evolve_cp_min * text when pokemon is released improved * Adding a message in the Readme about not supporting gym battles (#1453) * Pokemon plural (#1477) * Fix pokemon plural * Delte old pokemon file * [FEATURE] Api Wrapper to handle connection issues (#1459) * add an api wrapper managing (trying to) handle connections error, needs more testing * refine error testing * import fix * sleep less, lazy bum * change retry parameter as an optional argument * Magikarp twice in release block (#1486) * Fix typo in filename (#1494) * Add name to CONTRIBUTORS * Add name to CONTRIBUTORS * Fix typo in catch_visible_pokemon_worker filename * Add missing renamed catch_visible_pokemon_worker to repository * [Bug fixes] Further checking for the api wrapper response (#1499) * further checking for the api response * make sure to pop the request_callers field first * comment * new PolylineWalker(StepWalker) - [was #990] (#1467) * new PolylineWalker(StepWalker) Refactoring in the context of the new walker/navigator concept Fixes: - PolylineWalker class renamed to Polyline - new class PolylineWalker(StepWalker) - change few tests * fixed imports * Added further release functionality (#1472) * Added functionality to keep Pokemons based on IV and CP at the same time. * Fixed example config and _validate_keep_best_config * Added secondary criteria IV when CP is equal and the contrasting case. * Removed unused import * Use type unicode for argument location (#1503) * Use type unicode for argument location Fix for issue with invalid value for location argument, e.g.: invalid value: u'Pra\xe7a' when it contains special characters like "ç". * Parse location for both command line and json Will now correctly parse location both from command line and JSON file. * Better naming for function to parse unicode str * Added circle avoidance (#1515) * Added circle avoidance * Changed to add_config * Changed path to recent_forts. Put config keys into spin_forts key * cp_min -> evolve_cp_min in config.json.example * Update README.md * annoying bug (#1559) Added if try_cnt > 1: to remove the annoying count in logs due to server latency or something The response that comes back is "52" from api. * Throttling api requests. Reverting log change (#1562) * Egg incubation improvements (#1526) * Moved egg hatching to incubation worker, added feedback for users * old response handling and updated readme * Fix evolve_all and use_lucky_egg (#1541) Fixed issues where evolve_all would not run. Also fixed use_lucky_egg so that it only runs on the first tick. Refactored so the EvolveAllWorker._should_run function requires less logic (optimization) * Event system for logs and web socket communication (#1523) * Event system implementation * some web socket work * refactored the event system for clean ups * added socketio_client to requirements * let's not run event system setup yet and remove some tests * add possibility to set the event level in event system * some event system examples as comments * fixed handler and rudimentary version of real logging handler * better logging * fixing type in example about emitting events * added host and port configuration for websocket server instead of hardcoding them * added flask to requirements.txt * Revert "Egg incubation improvements" (#1565) * Reduce log spam when moving towards forts (#1566) * Fixed import error in api_wrapper (#1561) * Moved egg hatching to incubation worker, added feedback for users (#1568) * Refactoring function to get forts (#1578) * Refactoring function to get forts * Optionally sort by distance * Update pokemon_transfer_worker.py (#1571) * Update pokemon_transfer_worker.py Grammer correction. * Update pokemon_transfer_worker.py * adding support for embedded config keys and fixing circle prevention * CatchVisiblePokemonWorker now catches pokemon from lures (#1591) * CatchVisiblePokemonWorker catches from lures * Fix typos * Allow worker order to be more easily customised in future with PokemonGoBot (#1600) * Make SeenFortWorker top level (#1601) * CatchVisiblePokemonWorker catches from lures * Pull out SpinNearestFort into its own top level worker. Remove pokemon catching behavior * Removing unused reference * Moving MoveToFort to the top level (#1605) * Moving MoveToFort to the top level * Fixing bad import * Consolidate similar meaning configuration keys properly inside another key (#1590) * adding support for embedded config keys and fixing circle prevention * forgot to fix this config * refactor nested config system to support flag likes `--forts.something.anything` This example `--forts.something.anything` would be parsed as `config.forts_something_anything`. And in the JSON config it should be like this: ``` ‘forts’: { ‘something’: { ‘anything’: 1 } } ``` * add fix_nested_config(config) call * update missing usage of `config.avoid_circles` -> `config.forts_avoid_circles` * removed pdb, sorry * fixing buggy merge, i'm sorry * one last fix to nested config for fort spinning * other small fix to nested config * Fixed args PolylineWalker to match the super StepWalker class (#1621) * * Removed pokemongo_bot/polyline_stepper.py - old Stepper() class * Fixed args PolylineWalker to match the super StepWalker class * Added a check to Polynine() point tinitalization, if no route was found then, we will return no points between orig, dest thus will walk in straight line - expected behaviour will teleport in small steps * * fix typo * "evolve_captured" is now using a list instead of a boolean (#1532) * "evolve_captured" is now using a list instead of a boolean, working the same way as "evolve_all" * parse error with format details when "evolve_captured" is not a string, or is the string "true" or "false" * Extract CatchLuredPokemonWorker from PokemonCatchWorker and improved worker order (#1627) * extracted lure catch worker from pokemon catch * removing information less logs * little refactoring to catch lured pokemon worker * Fix 'with' statement mistakes (#1641) * Update catch_visible_pokemon_worker.py * Update __init__.py * using get_cell_ids from pgoapi package * trying to fix annoying log * ok, I give up on log organisation * Randomize `normalized_reticle_size` and `spin_modifier` parameter for `catch_pokemon` api (#1205) * the MoveToFortWorker should always go to the nearest fort (#1666) * Fix transfer worker not triggered for last pokemon (#1664) * Making the SpiralNavigator a worker (#1683) * Making the SpiralNavigator a worker * Passing pylint * Passing linter * fixes Polyline class to handle a case in which google is returning only one point (#1674) * Fixes: https://github.com/th3w4y/PokemonGo-Bot/issues/27 * Fixes: PolylineStepWalker walks for only one seconds #28 https://github.com/th3w4y/PokemonGo-Bot/issues/28 by adding a while destination nat reached loop * fixes typo * Revert "Making the SpiralNavigator a worker" (#1698) * feat: show xp after catching pokemons (#1700) * Update config.json.pokemon.example (#1711) config.json.pokemon.example was missing several fields present in config.json.example * Huge clean-up: PEP8, sort imports, remove deprecated and unused imports (#1697) * SoftBan Worker (#1724) * created a softban worker * only delete key from dict if it is there * pep8 stuff * Improve docker usage; use docker-compose for starting the PokemonGo-Bot ecosystem (#1669) * update Dockerfile; install missed python-protobuf package; add CMD as a way of defining default arguments for the ENTRYPOINT * remove CMD command, default config parameter is also set in pokecli.py * improve docker usage; add single container run for the webUI; add docker-compose.yml for starting the bot ecosystem with one command * fix "How to run with Docker" link * fix timezone setting: send timezone arg to the docker image build process, e.g. "docker build --build-arg timezone=Europe/Berlin -t pokemongo-bot ." * adding a duplicate of SeenFortWorker before MoveToFortWorker this ensures we interact with forts while we are moving to other forts * Show Pokestop names (#1671) * Restore the ability for a user to see Pokestop names. Default to off. * Use the add_config function for forts.show_name (now default to true) * Move fort_details function into cell_workers init module * Forgot to pass bot reference * Catching lured pokemon should use same fort_details API * REmove config option. Always show Pokestop name. * Move away from KeyError handling as per TheSaviour's suggestion * fixing wrong import * No longer caching things on the worker. Pulling straight from the bot instance (#1747) * Creating an instance of the workers only on startup (#1750) * Don't try to release pokemons in forts (#1751) * * Always report session summary even on crash (#1759) * Prevent crash checking session (#1754) * Prevent crash when check session * Fix function call * [FIX] use_lucky_egg (#1774) Changes to the tick_count caused the use_lucky_egg to not run. Since the tick_count is incremented prior to running the workers, the tick_count will be 1 on the first tick * add a flag to enable user to choose if he wants to walk to spin forts (#1772) * add a flag to enable user to choose if he wants to walk to spin forts that are far away * updated config example * Display Fort Name instead of Fort ID in Log (#1801) Displays the fort name in the CLI instead of the fort id. Easier to read, better to look at than a hash. * Update incubate_eggs_worker.py (#1862) Fix: variable 'pokemon_data' referenced before assignment * Improved some code formatting & fixed unicode issue with the logger. (#1839) * Improved some code formatting & fixed unicode issue with the logger. * Corrected formatting of log method & improved formatting of spiral_navigator.py * Upgrade the capture logic for VIP pokemons! (#1807) * This fix a small bug when user didn't update their config file for VIP setting (#1874) * [FEATURE] Path Navigator (#1457) Adds a navigator that walks along specified points. * fixing a typo that causes a NameError exception (#1898) * Adding a TreeConfigBuilder and tests (#1901) * Adding a TreeConfigBuilder and tests * Adding mock to the requirements * not actually using mock or patch * Egg Incubation - IV fix and UnboundLocalError fix (#1777) * fixes for ivs and bad var * Custom response/early return for error * added check for blank ids * added temp lists for duplication mitigation * Removing Worker suffix on workers (#1914) * Renaming more workers to make grammatical sense (#1915) * Improved Path Navigator, Now Supports geopositioning resolution (#1917) * Refactored Path Navigator, now supports geopositioning resolution * Update path example config, for new format * Fixed typo in dict * Fixed Ref * Possibility to set another config with run script (#1899) * Location cache check. If start position differs, don't use the cache. (#1932) * Making the navigators workers (#1933) * Adding the navigator to the list of workers (#1950) * Updated item_filter in config.json.example to use item names instead item id's (#1733) * Updated item_filter in config.json.example to use item names instead of item id's * Config.json item_list verification * Merge remote-tracking branch 'upstream/dev' into dev Conflicts: pokemongo_bot/cell_workers/recycle_items.py * Adding recycle_items back Changed error return type * Configure the tasks from config.json (#1956) * Configure the tasks from config.json * Linking error to wiki * Removing config for catch_pokemon (#1963) * Loop over an array of old flags (#1964) * Removing config forts_spin * Removing hatch_eggs from config * One more for hatch_eggs * Removing config for release_pokemon * One more for release_pokemon * Removing config for softban_fix * Removing config for forts.spin * Removing config for forts.move_to_spin * Supporting task level configuration (#1979) * Supporting task level configuration * Updating sample config files * Providing example of how to configure tasks in the example configs * Adding a task base class (#1983) * Re-enable item ID's in the item_filter. (#1986) * Allows users to enter both item ID's and item names in the item_filter. * no message * Allow to collect level up rewards (#2004) * Foundation for remote control of the bot over websocket (#2000) * improved websocket and logging handler * added support for remote command execution through websocket and a player_info call example * adding a missing variable * only execute remote command if it exists and is callable, else return command not found * wait forever instead of 5 secs in an infinite loop * Tasks now extend a base task (#2007) * Moving evolve_speed to task configuration * Moving use_lucky_egg to be a task configuration * Fixing bug in evolve all. Fixes #2019 * Removing unused navigator switch * Avoid transferring favorite pokemons (#2038) * Move follow path task config (#2044) * Refactor (#1587) * fixing `.get` call (closes #2082) * Log location as a str (#1825) We already have location as an encoded str in location_str and most calls to log pass a string as argument, so this is a bit more consistent. * Update README.md * Giving errors when specifying navigator cli arguments (#2126) * [DEV FEATURE] Test framework .... beginning (#1682) * add mock and nose to the dependencies * added unit tests for the api_wrapper * add testing to travis build * fixing path ? * pylint error fixed * adding myself to contributors * add test for the step_walker * add runtime error for big distances * change travis, nosetests should look for tests in all the folders * Getting rid of nose, rename some files, add 'timeout_decorator' to the requirements * update travis.yml * changed run_tests script from bash file to python file * revert file changes * skipping failing test * fix another test * some style/import improvment * revert SKIP_TIMEOUT * remove run_tests.py * move tests cases into main test folder * refactor some code api_wrapper_test * refactor and location parser * test is failing add a FIXME tag * location is now unicode friendly * Fix throw type always normal when trying to catch pokemon (#2130) * Warns if there aren't sufficient space left for loot. (#2137) * Warns if there aren't sufficient space left for loot. SpinFort will terminate silently, and users will not notice that SpinFort is skipping due to the lack of space. As a result, it sends out requests to Niantic even faster than before becuase tasks are looping faster and doing noops. More server busy (error 52) errors appear in the log without an explanation of what's actually going on. * improve readability of should_run for SpinFort and MoveToFort * [Feature] Detect maximum cluster and move (#1993) * adapted to new commits * added config changed gitignore * added config * locked versions * typo * account for task management change * moved find_cluster to utils. follow_cluster now 1 task * added test for follow_cluster * added mock requirements * trying to get travis to build * trying to get travis to build * added search for lured cluster functionality * adapted tests * removed double specification * only use berries on VIP pokemon if catch rate is less than 90% #2135 (#2138) * [FIX-Config] 'use_lucky_egg' should not be true in the exmple config file (#2105) * Update README.md * Added check for valid keep_best_iv amount (#2150) * Cluster Selection so it doesn't jump from cluster to cluster (#2153) * added secondary criteria so it doesn't jump from cluster to cluster when they're equally large * added secondary criteria so it doesn't jump from cluster to cluster when they're equally large * updated cluster example * Adding an Anyball item to the release config (#2140) Adding an Anyball item to the release config * removed wrong log (#2160) * Revert "Adding an Anyball item to the release config" (#2166) * This should fix issue: (#2185) MoveToFort(self.bot).work() TypeError: __init__() takes exactly 3 arguments (2 given) * Better enforce rules about Pokemons to retain. (#2073) Rules about maximum CP and IV to transfer Pokemons are applied also when keeping the best ones. * Fix incorrect config value for HandleSoftban. (#2191) * Moving item_filter to be a task level configuration * Moving evolve_all to be a task level configuration * Moving evolve_cp_min to be a task level configuration * Added nickname worker (#1850) * Adding a SleepSchedule worker. Pause for some time every day (#2193) * Added Sleeper worker * changed Sleep worker name to SleepSchedule * fixed wrong import names * changed name in log * Removing nickname pokemon from the example config * Remove MoveToFort from the FollowPath example config (#2203) FollowPath will not work properly while also using MoveToFort. * Using the logger instead of print in EvolveAll * fixed pokemon transfer so that "keep_best_*" filters can work again without requiring to be combined with "release_below_*" rules (#2215) * Let the user know that the maps api key exceeded its limits. (#1989) * Change FortID to FortName (#2249) Making it more human readable. I did not test this change, just used the same field from movetofort line 41 and spin fort line 33. * Request meta cell data once every 5 seconds (#2171) This solve the Niantic "scan for pokemon" throttling without making the bot very slow. * Use Default map_object_cache_time if not specified * Change egg hatching text (#2258) * [FIX] Improper use of exception (#2246) * adapted improper exception use * beautified * fixed logic error * Add optional simple lure attraction feature (#2257) * Add lure attraction params in default config * Update CONTRIBUTORS.md * Add simple lure attraction feature (move2fort) * Update move_to_fort.py * Dev - Fixed the loss of fort data (updated) (#2269) * Keep fort data even if the server returns no fort data. Also replaced redundant code. * Making sure we only save fort data if the server returned multiple forts. * Update web to latest master commit (#2274) The current dev commit has an issue where it doesn’t show the number of candies. * Add missing curly bracket (#2282) Added missing curly bracket to tasks>MoveToFort>config * added param in config.json.pokemon.example. * web submodule updated to latest commit (#2289) * Modify SpiralTask to use 70m as stepsize and diameter as step_count (#2194) * Lowered the stepsize in Spiral navigator to more accurate 70m * Moved max_steps to task configuration and changed it to diameter * Added diameter to configuration example * Bugfix * Removed max_steps from cli * Added max_steps as legacy configuration * Made step size of follow_spiral more configureable * Changed default value for diameter * This is just a temp fix, The one added the configure param need make sure it's really work. * Fix instance where evolve_all is unicode - fixes #2281 (#2305) * Fix instance where evolve_all is unicode * Test for isinstance basestring rather than Unicode || str * [Feature] added keep pokemon for batch evolution (#2255) * added keep_for_evo * accounted for non evolable pokemon * additional logging * additional logging * moved get_candies to utils * disregard 2nd stage evolution pokemon * added sample configs * Supporting sending requests through an HTTP proxy (#2358) * Added proxy support Added proxy support from config.json and command line. with parameters: -prxip | --proxy_ip or proxy_ip from config.json for ipv4 format as string "aaa.bbb.ccc.ddd" -prxp| --proxy_port or proxy_por from config.json as int from 1 to 65536. * Added proxy support Added two additional parameters (proxy_ip and proxy_port) to add support for proxy. proxy_ip must be as string of ipv4 format: "aaa.bbb.ccc.ddd" , proxy_port must be a int from 1 to 65536, if proxy_ip is null or set to "" and proxy port is null or set to 0 proxy will be ignored and not used. * Moved proxy function to a method. Moved proxy function to a method. * Changed the name of method Changed from set_proxy_if_exists to setup_proxy * Revert "Dev Proxy support" (#2374) * Revert "[Feature] added keep pokemon for batch evolution" (#2380) * Adapt code to new PGoApi Code (#2357) * wip: fixing imports and tests * tests are passing * add tests, and modify calls to api accordingly * fix login bug * fix rebase * fix import error * Handle ThrottlingException * fix relogging errors * Refactor evolve_all worker (#2244) * Refactor evolve_all worker - Remove transfer of evolved pokemon (should be handle by transfer task) - Add order_by config flag to choose to evolve by iv or cp (default: cp) - Add evolve_iv_min as threshold for evolve by iv (order by cp under threshold) - Fix _validate_config not called before - Get candy list to test if enough candy in the bag - Filter out pokemon which can't be evolved * remove unnecessary debug lines * Add missing candy name * Use uncached inventory to have up to date amount of candy and list of pokemon * Fix candy name Add missing candies * Fix evolving logic: - replace "order_by" by "first_evolve_by" to choose if we prioritize "cp" or "iv" (default: "cp") - replace "evolve_cp_min" by "evolve_above_cp" (default: 500) - replace "evolve_iv_min" by "evolve_above_iv" (default: 0.8) - add "logic" to choose if we "evolve_above_cp" and/or "evolve_above_iv" - update config file * Rename EvolveAll to EvolvePokemon task name * Add error warning about task renaming * Add a test about tasl renaming * Fix task renaming warning * Update new api wrapper * delaying errors, and reducing noise (#2393) * Removed max_steps in the config pokemon sample. Also added FollowSpiral's new options (#2342) * Display stats in the terminal title (#2252) * Added UpdateTitleStats worker * Added UpdateTitleStats worker * Fixed return inconsistency in work method * :rocket: Massively improved pylint rate Cleaned ctypes unnecessary imports Moved initialization inside __init__ method * Fixed incorrect default value for min_interval * Added support for cygwin on Windows * Catch UnexpectedResponseException and retry (#2407) * Catch UnexpectedResponseException and retry * New func for UnexpectedResponse * Fixed merge conflicts. * Randomize spins for softban #2247 (#2253) * Randomize spins for softban #2247 * Update handle_soft_ban.py * fix(docker): correct web config file path (#2350) * Allow for 3-7 decimal points for coordinates (#2402) Some exports only provide three decimal point accuracy, and sometimes 4.440000 turns into 4.44, need to adjust the regex. * PokemonGo-Map Synergy (#1992) * Feature: Use PokemonGo-Map sqlite db to catch pokemon near you * added example config for move_to_map_pokemon * adapted new config format * Automatically update Map position * remove pokemon when encountered early * forgot to remove a log * minor fix * updated example config and added ignore config * change ignore config to a list * teleport to pokemon if walk option is 0 * added snipe option * Teleport back after sniped pokemon was caught * proper sniping * mark sniped pokemon as caught * forgot to remove print * minor bug fix * ignore max_distance when sniping * prioritize VIPs in a 10km radius * better prioritize vips * syntax error fix * set base priority for vips * move map config example to seperate file * use web api instead of sqlite db * fix datetime format * huge code cleanup * forgot to snipe * add vips to catch as default * default priority for vips * only mark pokemon as caught when it really was caught * bugfix * bugfix #2 * i should go to bed * add option to disable map update * updated example map config to match default example * improve pylint result, fix catch recognition * more code clean up * better config example * dump caught pokemon to json file to prevent targeting them on restart * check if we got pokeballs to use * remove print * fix item_inventory_count returnin None instead of 0 * if we only have ultraballs and the target is not a vip don't snipe/walk * remove gender symbols * fix Mr. Mime * vip wrong order fix * bugfix * log error when JSON decoding fails * handle base64 error * Return type None on nested call was breaking details display (#2416) * Adding a section on analytics and metrics to the Readme (#2434) * Fix to display stats on iterm2 terminal (#2440) * Fix #2442 - should_retry_throttle isn't defined (#2461) * Fix #2442 Variables weren't correctly defined * Fix typo * Replace all `logger.log` calls with events! (#2173) * bye bye `logger.log`, hello event system! * fixing travis build * trying to fix travis build * test fixes * updating remaining `logger.log` calls that should be replaced * typo * typos in IncubateEggs event * improved fort loot event data * fixing update_location event's distance unit * fixing some events and log stuff * adding missing fort_name parameter to lured_pokemon_found event * fixing a variable inside an event formatted string * fixing typos and utf8 * trying to fix tests with regards to float precision * adding command to print all registered events and their parameters * fixing tests yet again * trying to fix unicode issues, arrgh!!! * added a move to lured fort event * better distance text in move to fort and fixing utf8 in spin fort task * removing print from websocket server * start embedded server before creating the socketio_handler * I hate unicode * rename and sleep events * refactoring in how we emit events to avoid code repetition * PokemonCatch task inherits from BaseTask * go away, dirty logger.log! * pep8 and removed logging handler name attribute * good bye for the remaining logger.log calls * bye logger module * no more logger imports! * removed last few loggers * removing secret file and fixed variable name in follow cluster * fixing kwargs for event emit * trying to fix unicode handling one more time * now it works! * fixing more logs and removing debug unicode string * no logs on websocket server yet * adding a script to start a standalone websocket server * more adjusted in websocket to support multiuser * adding a fallback to logger.log issues a very verbose deprecation warning * putting back compatibility with json based web ui * correct parsing evolve_all (#2455) * correct parsing evolve_all Previously, ``` "evolve_all": "Pidgey, Caterpie, Weedle", ``` would only evolve Pidgey. This PR fix that. * fix parsing evolve_captured * Remove max_steps from examples and set EvolveAll to EvolvePokemon (#2430) * Adding Raven to send exception reports to Sentry (#2514) * Adding Raven to send exception reports to Sentry * Removing test exception * Removing incompatible python3 analytics library * Using logger.log * Using the correct logger * changing license from MIT to GPLv3 * Updated README.md to state bot status (#2586) * Added bot broken message at top of page. * Changed REAME.md to mirror #2590 * Reordered * Handling KeyboardInterrupt and some other exceptions (#2599) --- .dockerignore | 4 + .gitignore | 36 +- .gitmodules | 2 +- .travis.yml | 4 +- CONTRIBUTORS.md | 53 + Dockerfile | 12 +- LICENSE | 674 ++++++++ README.md | 202 +-- config.json.example | 17 - configs/config.json.cluster.example | 123 ++ configs/config.json.example | 131 ++ configs/config.json.map.example | 361 +++++ configs/config.json.path.example | 101 ++ configs/config.json.pokemon.example | 357 +++++ configs/path.example.json | 6 + data/pokemon.json | 2 +- docker-compose.yml | 20 + install.sh | 19 + pokecli.py | 521 ++++-- pokemongo_bot/__init__.py | 1245 ++++++++++----- pokemongo_bot/api_wrapper.py | 157 ++ pokemongo_bot/cell_workers/__init__.py | 22 +- pokemongo_bot/cell_workers/base_task.py | 30 + .../cell_workers/catch_lured_pokemon.py | 51 + .../cell_workers/catch_visible_pokemon.py | 48 + .../cell_workers/collect_level_up_reward.py | 74 + .../cell_workers/evolve_all_worker.py | 240 --- pokemongo_bot/cell_workers/evolve_pokemon.py | 168 ++ pokemongo_bot/cell_workers/follow_cluster.py | 85 + pokemongo_bot/cell_workers/follow_path.py | 103 ++ pokemongo_bot/cell_workers/follow_spiral.py | 116 ++ pokemongo_bot/cell_workers/handle_soft_ban.py | 69 + pokemongo_bot/cell_workers/incubate_eggs.py | 207 +++ .../cell_workers/initial_transfer_worker.py | 75 - pokemongo_bot/cell_workers/move_to_fort.py | 150 ++ .../cell_workers/move_to_fort_worker.py | 40 - .../cell_workers/move_to_map_pokemon.py | 197 +++ .../cell_workers/nickname_pokemon.py | 101 ++ .../cell_workers/pokemon_catch_worker.py | 689 +++++--- pokemongo_bot/cell_workers/recycle_items.py | 63 + .../cell_workers/seen_fort_worker.py | 138 -- pokemongo_bot/cell_workers/sleep_schedule.py | 108 ++ pokemongo_bot/cell_workers/spin_fort.py | 157 ++ .../cell_workers/transfer_pokemon.py | 233 +++ .../cell_workers/update_title_stats.py | 233 +++ pokemongo_bot/cell_workers/utils.py | 148 +- pokemongo_bot/constants.py | 2 + pokemongo_bot/event_handlers/__init__.py | 2 + .../event_handlers/logging_handler.py | 18 + .../event_handlers/socketio_handler.py | 30 + pokemongo_bot/event_manager.py | 65 + pokemongo_bot/health_record/__init__.py | 3 + pokemongo_bot/health_record/bot_event.py | 68 + pokemongo_bot/human_behaviour.py | 39 +- pokemongo_bot/lcd.py | 12 +- pokemongo_bot/logger.py | 40 +- pokemongo_bot/metrics.py | 113 ++ pokemongo_bot/polyline_stepper.py | 56 - pokemongo_bot/socketio_server/__init__.py | 0 pokemongo_bot/socketio_server/app.py | 32 + pokemongo_bot/socketio_server/runner.py | 34 + pokemongo_bot/step_walker.py | 65 + pokemongo_bot/stepper.py | 158 -- pokemongo_bot/test/__init__.py | 0 pokemongo_bot/test/follow_cluster_test.py | 42 + .../test/resources/example_forts.pickle | 1415 +++++++++++++++++ pokemongo_bot/test/sleep_schedule_test.py | 107 ++ pokemongo_bot/test/socketio-client.py | 15 + pokemongo_bot/tree_config_builder.py | 36 + .../{polyline_walker => walkers}/__init__.py | 1 + .../polyline_generator.py} | 72 +- .../polyline_generator_tester.py} | 10 +- pokemongo_bot/walkers/polyline_walker.py | 31 + pokemongo_bot/websocket_remote_control.py | 53 + pokemongo_bot/worker_result.py | 3 + pylint-recursive.py | 60 + release_config.json.example | 174 -- requirements.txt | 12 +- run.sh | 18 + setup.py | 1 - tests/__init__.py | 23 + tests/api_wrapper_test.py | 123 ++ tests/base_task_test.py | 40 + tests/location_parser_test.py | 33 + tests/step_walker_test.py | 73 + tests/tree_config_builder_test.py | 85 + tests/update_title_stats_test.py | 131 ++ travis-pythoncheck.py | 48 - web | 2 +- ws_server.py | 26 + 90 files changed, 9036 insertions(+), 1927 deletions(-) create mode 100644 .dockerignore create mode 100644 CONTRIBUTORS.md create mode 100644 LICENSE delete mode 100644 config.json.example create mode 100644 configs/config.json.cluster.example create mode 100644 configs/config.json.example create mode 100644 configs/config.json.map.example create mode 100644 configs/config.json.path.example create mode 100644 configs/config.json.pokemon.example create mode 100644 configs/path.example.json create mode 100644 docker-compose.yml create mode 100755 install.sh mode change 100755 => 100644 pokecli.py create mode 100644 pokemongo_bot/api_wrapper.py create mode 100644 pokemongo_bot/cell_workers/base_task.py create mode 100644 pokemongo_bot/cell_workers/catch_lured_pokemon.py create mode 100644 pokemongo_bot/cell_workers/catch_visible_pokemon.py create mode 100644 pokemongo_bot/cell_workers/collect_level_up_reward.py delete mode 100644 pokemongo_bot/cell_workers/evolve_all_worker.py create mode 100644 pokemongo_bot/cell_workers/evolve_pokemon.py create mode 100644 pokemongo_bot/cell_workers/follow_cluster.py create mode 100644 pokemongo_bot/cell_workers/follow_path.py create mode 100644 pokemongo_bot/cell_workers/follow_spiral.py create mode 100644 pokemongo_bot/cell_workers/handle_soft_ban.py create mode 100644 pokemongo_bot/cell_workers/incubate_eggs.py delete mode 100644 pokemongo_bot/cell_workers/initial_transfer_worker.py create mode 100644 pokemongo_bot/cell_workers/move_to_fort.py delete mode 100644 pokemongo_bot/cell_workers/move_to_fort_worker.py create mode 100644 pokemongo_bot/cell_workers/move_to_map_pokemon.py create mode 100644 pokemongo_bot/cell_workers/nickname_pokemon.py create mode 100644 pokemongo_bot/cell_workers/recycle_items.py delete mode 100644 pokemongo_bot/cell_workers/seen_fort_worker.py create mode 100644 pokemongo_bot/cell_workers/sleep_schedule.py create mode 100644 pokemongo_bot/cell_workers/spin_fort.py create mode 100644 pokemongo_bot/cell_workers/transfer_pokemon.py create mode 100644 pokemongo_bot/cell_workers/update_title_stats.py create mode 100644 pokemongo_bot/constants.py create mode 100644 pokemongo_bot/event_handlers/__init__.py create mode 100644 pokemongo_bot/event_handlers/logging_handler.py create mode 100644 pokemongo_bot/event_handlers/socketio_handler.py create mode 100644 pokemongo_bot/event_manager.py create mode 100644 pokemongo_bot/health_record/__init__.py create mode 100644 pokemongo_bot/health_record/bot_event.py create mode 100644 pokemongo_bot/metrics.py delete mode 100644 pokemongo_bot/polyline_stepper.py create mode 100644 pokemongo_bot/socketio_server/__init__.py create mode 100644 pokemongo_bot/socketio_server/app.py create mode 100644 pokemongo_bot/socketio_server/runner.py create mode 100644 pokemongo_bot/step_walker.py delete mode 100644 pokemongo_bot/stepper.py create mode 100644 pokemongo_bot/test/__init__.py create mode 100644 pokemongo_bot/test/follow_cluster_test.py create mode 100644 pokemongo_bot/test/resources/example_forts.pickle create mode 100644 pokemongo_bot/test/sleep_schedule_test.py create mode 100644 pokemongo_bot/test/socketio-client.py create mode 100644 pokemongo_bot/tree_config_builder.py rename pokemongo_bot/{polyline_walker => walkers}/__init__.py (51%) rename pokemongo_bot/{polyline_walker/polyline_walker.py => walkers/polyline_generator.py} (51%) rename pokemongo_bot/{polyline_walker/polyline_tester.py => walkers/polyline_generator_tester.py} (83%) create mode 100644 pokemongo_bot/walkers/polyline_walker.py create mode 100644 pokemongo_bot/websocket_remote_control.py create mode 100644 pokemongo_bot/worker_result.py create mode 100644 pylint-recursive.py delete mode 100644 release_config.json.example create mode 100755 run.sh create mode 100644 tests/__init__.py create mode 100644 tests/api_wrapper_test.py create mode 100644 tests/base_task_test.py create mode 100644 tests/location_parser_test.py create mode 100644 tests/step_walker_test.py create mode 100644 tests/tree_config_builder_test.py create mode 100644 tests/update_title_stats_test.py delete mode 100644 travis-pythoncheck.py create mode 100755 ws_server.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..b08db7b967 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.idea +.git* +**/*config.json +**/*userdata.js \ No newline at end of file diff --git a/.gitignore b/.gitignore index cb30b978fa..a12509c322 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,6 @@ var/ .pydevproject .settings/ - # Installer logs pip-log.txt pip-delete-this-directory.txt @@ -98,19 +97,26 @@ ENV/ .idea/ # Personal load details -config.json src/ -info.json -inventory.json -pokedex.json -web/catchable.json -web/catchable-*.json -web/location-*.json -web/inventory-*.json -web/location.json -web/userdata.js +web/ data/last-location*.json -data/catch-ignore.yml -release_config.json -web/userdata.js -location.json +data/cells-*.json +data/map-caught-*.json + +# Multiple config +configs/* +!configs/config.json.example +!configs/release_config.json.example +!configs/config.json.pokemons.example +!configs/config.json.pokemon.example +!configs/config.json.path.example +!configs/config.json.map.example +!configs/path.example.json +!config.json.cluster.example + +# Virtualenv folders +bin/ +include/ + +# Pip check file +pip-selfcheck.json diff --git a/.gitmodules b/.gitmodules index 7930970692..612529e7ad 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "origin"] +[submodule "web"] path = web url = https://github.com/OpenPoGo/OpenPoGoWeb.git \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 3a9d0b9443..aef60fa3c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,4 +10,6 @@ addons: install: - pip install -r requirements.txt - pip install pylint -script: "python travis-pythoncheck.py" +script: + - python pylint-recursive.py + - python -m unittest discover -v -p "*_test.py" diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000000..dfd8a24af2 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,53 @@ +## Contributors + * eggins [first pull request] + * crack00r + * ethervoid + * Bashin + * tstumm + * TheGoldenXY + * Reaver01 + * rarshonsky + * earthchie + * haykuro + * 05-032 + * sinistance + * CapCap + * mzupan + * gnekic(GeXx) + * Shoh + * luizperes + * brantje + * VirtualSatai + * dmateusp + * jtdroste + * msoedov + * Grace + * Calcyfer + * asaf400 + * guyz + * DavidK1m + * budi-khoirudin + * riberod07 + * th3w4y + * Leaklessgfy + * steffwiz + * pulgalipe + * BartKoppelmans + * phil9l + * VictorChen + * AlvaroGzP + * fierysolid + * surfaace + * surceis + * SpaceWhale + * klingan + * reddivision + * DayBr3ak + * kbinani + * mhdasding + * MFizz + * NamPNQ + * z4ppy.bbc + * matheussampaio + * Abraxas000 + * lucasfevi diff --git a/Dockerfile b/Dockerfile index a1bdd61406..58c45cd02f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,13 @@ FROM python:2.7-onbuild -ENTRYPOINT ["python", "pokecli.py"] +ARG timezone=Etc/UTC +RUN echo $timezone > /etc/timezone \ + && ln -sfn /usr/share/zoneinfo/$timezone /etc/localtime \ + && dpkg-reconfigure -f noninteractive tzdata + +RUN apt-get update \ + && apt-get install -y python-protobuf + +VOLUME ["/usr/src/app/web"] + +ENTRYPOINT ["python", "pokecli.py"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..9cecc1d466 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index d5b44ae8a1..8fc2ed8741 100644 --- a/README.md +++ b/README.md @@ -1,174 +1,66 @@ -

- - Logo - -

- -

- Slack -

# PokemonGo-Bot -The Pokemon Go Bot, baking with community. -## Niantic Changes, currently there's big progress, we are waiting for the API update pkmngodev/Unknown6. +PokemonGo bot is a project created by the [PokemonGoF](https://github.com/PokemonGoF) team. +The project is currently setup in two main branches. `dev` and `master`. -Niantic have changed API responses, meaning that this bot and anything accessing the API though POGOProtos is currently broken. A number of developers from /r/pokemondev are working to address this and come up with a fix for this issue, find the [current status here](https://www.reddit.com/r/pokemongodev/comments/4w1cvr/pokemongo_current_api_status/) -## Help Needed on [Desktop Version](https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) -## Help with the project [Dev Bot](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Develop-PokemonGo-Bot) -## Project Chat -We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) -## Breaking Changes -You need modify config.json (config.json.pokemon.example for example) then python pokecli.py --config configs/config.json +## Niantic Changes + Niantic have changed API responses, meaning that this bot and anything accessing the API though POGOProtos is currently broken. A number of developers from /r/pokemondev are working to address this and come up with a fix for this issue, find the [current status here](https://www.reddit.com/r/pokemongodev/comments/4w1cvr/pokemongo_current_api_status/) -[More details about config file](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files) -Please clean up your old clone if you have issue, and following the [install instruction](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation). +We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) -## About dev/master Branch -Dev branch has the most up-to-date features, but be aware that there might be some broken changes. Your contribution and PR for fixes are warm welcome. -Master branch is the stable branch. -No PR on master branch to keep things easier. ## Table of Contents -- [Project Chat](#project-chat) - [Features](#features) -- [TODO List](#todo-list) -- [Usage](#usage) +- [Wiki](#wiki) - [Credits](#credits) - [Donation](#donation) + ## Features - * Search Fort (Spin Pokestop) - * Catch Pokemon - * Release low cp pokemon - * Walking as you - * Limit the step to farm specific area for pokestops - * Use the ball you have to catch, don't if you don't have - * Rudimentary IV Functionality filter - * Auto switch mode (Full of item then catch, no ball useable then farm) - * Ignore certain pokemon filter - * Use superior ball types when necessary - * When out of normal pokeballs, use the next type of ball unless there are less than 10 of that type, in which case switch to farm mode - * Drop items - * Pokemon catch filter - * Google Map API key setup - * Show all objects on map (In Testing) - * Evolve pokemons (Code in, Need input, In Testing) - * Incubate eggs - * Hatch eggs - * Pokemon transfer filter - -## TODO List - -- [ ] Standalone Desktop APP +- [x] GPS Location configuration +- [x] Search Pokestops +- [x] Catch Pokemon +- [x] Determine which pokeball to use (uses Razz Berry if the catch percentage is low!) +- [x] Exchange Pokemon as per configuration +- [x] Evolve Pokemon as per configuration +- [x] Auto switch mode (Inventory Checks - switches between catch/farming items) +- [x] Limit the step to farm specific area for pokestops +- [x] Rudimentary IV Functionality filter +- [x] Ignore certain pokemon filter +- [x] Adjust delay between Pokemon capture & Transfer as per configuration +- [ ] Standalone Desktop Application +- [x] Hatch eggs +- [x] Incubate eggs - [ ] Use candy -- [ ] Softban Bypass (In Development) +- [ ] Inventory cleaner ## Gym Battles This bot takes a strong stance against automating gym battles. Botting gyms will have a negative effect on most players and thus the game as a whole. We will thus never accept contributions or changes containing code specific for gym battles. -## Installation -[Getting started guide](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Getting-Started) -[Jump right into installing](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation) - -### Note on virtualenv -We recommend you use virtualenv, not only will this tool keep your OS clean from all the python plugins. -It also provide an virtual space for more than 1 instance! - -### Protobuf 3 installation Notes - -- OS X: `brew update && brew install --devel protobuf` -- Windows: Download protobuf 3.0: [here](https://github.com/google/protobuf/releases/download/v3.0.0-beta-4/protoc-3.0.0-beta-4-win32.zip) and unzip `bin/protoc.exe` into a folder in your PATH. -- Linux: `sudo apt-get install python-protobuf` - -### Note on branch -Please keep in mind that master is not always up-to-date whereas 'dev' is. In the installation note below change `master` to `dev` if you want to get and use the latest version. - -Make sure you install the following first: -[Requirements](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation) - -### Google Maps API Bot Tracker -[Wiki on using the bot web folder](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page) - -### FAQ -[Tips & Tricks](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ) - -## How to run with Docker -[Wiki on how to use Docker](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/How-to-run-with-Docker) - -## How to add/discover new API -The example is [here](https://github.com/PokemonGoF/PokemonGo-Bot/commit/46e2352ce9f349cc127a408959679282f9999585) - -1. Check the type of your API request in [POGOProtos](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/RequestType.proto) For example: RECYCLE_INVENTORY_ITEM - -2. Convert to the api call in pokemongo_bot/__init__.py, RECYCLE_INVENTORY_ITEM change to self.api.recycle_inventory_item - - ```python - def drop_item(self,item_id,count): - self.api.recycle_inventory_item(...............) - ``` - -3. Where is the param list? - You need check this [Requests/Messages/RecycleInventoryItemMessage.proto](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/Messages/RecycleInventoryItemMessage.proto) - -4. Then our final api call is - - ```python - def drop_item(self,item_id,count): - self.api.recycle_inventory_item(item_id=item_id,count=count) - inventory_req = self.api.call() - print(inventory_req) - ``` -5. You can now debug on the log to see if get what you need - -## FAQ -[Wiki Link](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ) - -### What's IV ? -Here's the [introduction](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Pokemon-IV) -Research Website [Nice Tool](https://thesilphroad.com/research) - -### What are the Item ID -[Wiki](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Item-ID's) - -##Softban -[Wiki](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Softban) - ---------- -## Contributors (Don't forget add yours here when you create PR) - * eggins -- The first pull request :) - * crack00r - * ethervoid - * Bashin - * tstumm - * TheGoldenXY - * Reaver01 - * rarshonsky - * earthchie - * haykuro - * 05-032 - * sinistance - * CapCap - * mzupan - * gnekic(GeXx) - * Shoh - * luizperes - * brantje - * VirtualSatai - * dmateusp - * jtdroste - * msoedov - * Grace - * Calcyfer - * asaf400 - * guyz - * DavidK1m - * budi-khoirudin - * riberod07 - * th3w4y - * Leaklessgfy - * GregTampa - * AlexRatman - -------- +## Analytics +This bot is very popular and has a vibrant community. Because of that, it has become very difficult for us to know how the bot is used and what errors people hit. By capturing small amounts of data, we can prioritize our work better such as fixing errors that happen to a large percentage of our user base, not just a vocal minority. + +Our goal is to help inform our decisions by capturing data that helps us get aggregate usage and error reports, not personal information. To view the code that handles analytics in our master branch, you can use this [search link](https://github.com/PokemonGoF/PokemonGo-Bot/search?utf8=%E2%9C%93&q=BotEvent). + +If there are any concerns with this policy or you believe we are tracking something we shouldn't, please open a ticket in the tracker. The contributors always intend to do the right thing for our users, and we want to make sure we are held to that path. + +If you do not want any data to be gathered, you can turn off this feature by setting `health_record` to `false` in your `config.json`. + +## Wiki +All information on [Getting Started](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Getting-Started) is available in the [Wiki](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/)! +- __Installation__ + - [Requirements] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#requirements-click-each-one-for-install-guide) + - [How to run with Docker](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/How-to-run-with-Docker) + - [Linux] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-linux) + - [Mac] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-mac) + - [Windows] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-windows) +- [Develop PokemonGo-Bot](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Develop-PokemonGo-Bot) +- [Configuration-files](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files) +- [Front end web module - Google Maps API] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page)) +- [Docker Usage](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ#how-to-run-with-docker) +- [FAQ](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ) + +To ensure that all updates are documented - [@eggins](https://github.com/eggins) will keep the Wiki updated with the latest information on installing, updating and configuring the bot. + ## Credits - [tejado](https://github.com/tejado) many thanks for the API - [Mila432](https://github.com/Mila432/Pokemon_Go_API) for the login secrets diff --git a/config.json.example b/config.json.example deleted file mode 100644 index 3245708e6d..0000000000 --- a/config.json.example +++ /dev/null @@ -1,17 +0,0 @@ -{ - "auth_service": "google", - "username": "YOURACCOUNT@gmail.com", - "password": "YOURPASSWORD", - "location": "SOME LOCATION", - "gmapkey": "AGMAPAPIKEY", - "max_steps": 5, - "mode": "all", - "walk": 4.16, - "debug": false, - "test": false, - "initial_transfer": 0, - "location_cache": true, - "distance_unit": "km", - "item_filter": "101,102,103,104", - "evolve_all": "NONE" -} diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example new file mode 100644 index 0000000000..8d0d8f854f --- /dev/null +++ b/configs/config.json.cluster.example @@ -0,0 +1,123 @@ +{ + "auth_service": "google", + "username": "YOUR_USERNAME", + "password": "YOUR_PASSWORD", + "location": "SOME_LOCATION", + "gmapkey": "GOOGLE_MAPS_API_KEY", + "tasks": [ + { + "type": "HandleSoftBan" + }, + { + "type": "CollectLevelUpReward" + }, + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + }, + { + "type": "TransferPokemon" + }, + { + "type": "EvolvePokemon", + "config": { + "evolve_all": "none", + "first_evolve_by": "cp", + "evolve_above_cp": 500, + "evolve_above_iv": 0.8, + "logic": "or", + "evolve_speed": 20, + "use_lucky_egg": false + } + }, + { + "type": "RecycleItems", + "config": { + "item_filter": { + "Pokeball": { "keep" : 100 }, + "Potion": { "keep" : 10 }, + "Super Potion": { "keep" : 20 }, + "Hyper Potion": { "keep" : 30 }, + "Revive": { "keep" : 30 }, + "Razz Berry": { "keep" : 100 } + } + } + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort" + }, + { + "type": "FollowCluster", + "config": { + "radius": 50, + "lured": true + } + } + ], + "forts": { + "avoid_circles": true, + "max_circle_size": 50 + }, + "websocket_server": false, + "walk": 4.16, + "action_wait_min": 1, + "action_wait_max": 4, + "debug": false, + "test": false, + "health_record": true, + "location_cache": true, + "distance_unit": "km", + "reconnecting_timeout": 15, + "evolve_captured": "NONE", + "catch_randomize_reticle_factor": 1.0, + "catch_randomize_spin_factor": 1.0, + "catch": { + "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, + "// Example of always catching Rattata:": {}, + "// Rattata": { "always_catch" : true } + }, + "release": { + "any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}, + "// Example of always releasing Rattata:": {}, + "// Rattata": {"always_release": true}, + "// Example of keeping 3 stronger (based on CP) Pidgey:": {}, + "// Pidgey": {"keep_best_cp": 3}, + "// Example of keeping 2 stronger (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_iv": 2}, + "// Also, it is working with any": {}, + "// any": {"keep_best_iv": 3}, + "// Example of keeping the 2 strongest (based on CP) and 3 best (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_cp": 2, "keep_best_iv": 3} + }, + "vips" : { + "Any pokemon put here directly force to use Berry & Best Ball to capture, to secure the capture rate!": {}, + "any": {"catch_above_cp": 1200, "catch_above_iv": 0.9, "logic": "or" }, + "Lapras": {}, + "Moltres": {}, + "Zapdos": {}, + "Articuno": {}, + + "// S-Tier pokemons (if pokemon can be evolved into tier, list the representative)": {}, + "Mewtwo": {}, + "Dragonite": {}, + "Snorlax": {}, + "// Mew evolves to Mewtwo": {}, + "Mew": {}, + "Arcanine": {}, + "Vaporeon": {}, + "Gyarados": {}, + "Exeggutor": {}, + "Muk": {}, + "Weezing": {}, + "Flareon": {} + + } +} diff --git a/configs/config.json.example b/configs/config.json.example new file mode 100644 index 0000000000..20ef72e34e --- /dev/null +++ b/configs/config.json.example @@ -0,0 +1,131 @@ +{ + "auth_service": "google", + "username": "YOUR_USERNAME", + "password": "YOUR_PASSWORD", + "location": "SOME_LOCATION", + "gmapkey": "GOOGLE_MAPS_API_KEY", + "tasks": [ + { + "type": "HandleSoftBan" + }, + { + "type": "CollectLevelUpReward" + }, + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + }, + { + "type": "TransferPokemon" + }, + { + "type": "EvolvePokemon", + "config": { + "evolve_all": "none", + "first_evolve_by": "cp", + "evolve_above_cp": 500, + "evolve_above_iv": 0.8, + "logic": "or", + "evolve_speed": 20, + "use_lucky_egg": false + } + }, + { + "type": "RecycleItems", + "config": { + "item_filter": { + "Pokeball": { "keep" : 100 }, + "Potion": { "keep" : 10 }, + "Super Potion": { "keep" : 20 }, + "Hyper Potion": { "keep" : 30 }, + "Revive": { "keep" : 30 }, + "Razz Berry": { "keep" : 100 } + } + } + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort" + }, + { + "type": "MoveToFort", + "config": { + "lure_attraction": true, + "lure_max_distance": 2000 + } + }, + { + "type": "FollowSpiral", + "config": { + "diameter": 4, + "step_size": 70 + } + } + ], + "map_object_cache_time": 5, + "forts": { + "avoid_circles": true, + "max_circle_size": 50 + }, + "websocket_server": false, + "walk": 4.16, + "action_wait_min": 1, + "action_wait_max": 4, + "debug": false, + "test": false, + "health_record": true, + "location_cache": true, + "distance_unit": "km", + "reconnecting_timeout": 15, + "evolve_captured": "NONE", + "catch_randomize_reticle_factor": 1.0, + "catch_randomize_spin_factor": 1.0, + "catch": { + "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, + "// Example of always catching Rattata:": {}, + "// Rattata": { "always_catch" : true } + }, + "release": { + "any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}, + "// Example of always releasing Rattata:": {}, + "// Rattata": {"always_release": true}, + "// Example of keeping 3 stronger (based on CP) Pidgey:": {}, + "// Pidgey": {"keep_best_cp": 3}, + "// Example of keeping 2 stronger (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_iv": 2}, + "// Also, it is working with any": {}, + "// any": {"keep_best_iv": 3}, + "// Example of keeping the 2 strongest (based on CP) and 3 best (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_cp": 2, "keep_best_iv": 3} + }, + "vips" : { + "Any pokemon put here directly force to use Berry & Best Ball to capture, to secure the capture rate!": {}, + "any": {"catch_above_cp": 1200, "catch_above_iv": 0.9, "logic": "or" }, + "Lapras": {}, + "Moltres": {}, + "Zapdos": {}, + "Articuno": {}, + + "// S-Tier pokemons (if pokemon can be evolved into tier, list the representative)": {}, + "Mewtwo": {}, + "Dragonite": {}, + "Snorlax": {}, + "// Mew evolves to Mewtwo": {}, + "Mew": {}, + "Arcanine": {}, + "Vaporeon": {}, + "Gyarados": {}, + "Exeggutor": {}, + "Muk": {}, + "Weezing": {}, + "Flareon": {} + + } +} diff --git a/configs/config.json.map.example b/configs/config.json.map.example new file mode 100644 index 0000000000..e665d4c6da --- /dev/null +++ b/configs/config.json.map.example @@ -0,0 +1,361 @@ +{ + "auth_service": "google", + "username": "YOUR_USERNAME", + "password": "YOUR_PASSWORD", + "location": "SOME_LOCATION", + "gmapkey": "GOOGLE_MAPS_API_KEY", + "tasks": [ + { + "type": "HandleSoftBan" + }, + { + "type": "CollectLevelUpReward" + }, + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + }, + { + "type": "TransferPokemon" + }, + { + "type": "EvolvePokemon", + "config": { + "evolve_all": "NONE", + "evolve_cp_min": 300, + "evolve_speed": 20, + "use_lucky_egg": false + } + }, + { + "type": "RecycleItems", + "config": { + "item_filter": { + "Pokeball": { "keep" : 100 }, + "Potion": { "keep" : 10 }, + "Super Potion": { "keep" : 20 }, + "Hyper Potion": { "keep" : 30 }, + "Revive": { "keep" : 30 }, + "Razz Berry": { "keep" : 100 } + } + } + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort" + }, + { + "type": "MoveToMapPokemon", + "config": { + "address": "http://localhost:5000", + "max_distance": 500, + "min_time": 60, + "prioritize_vips": true, + "snipe": false, + "update_map": true, + "mode": "priority", + "catch": { + "==========Legendaries==========": 0, + "Aerodactyl": 1000, + "Snorlax": 1000, + "Articuno": 1000, + "Zapdos": 1000, + "Moltres": 1000, + "Dratini": 1000, + "Dragonair": 1000, + "Dragonite": 1000, + "Mewtwo": 1000, + "Mew": 1000, + + "==========Region Locked==========": 0, + "Farfetch'd": 1000, + "Kangaskhan": 1000, + "Mr. Mime": 1000, + "Tauros": 1000, + + "==========Very Rare==========": 0, + "Lapras": 900, + "Electabuzz": 900, + "Magmar": 900, + "Ditto": 900, + + "==========Starters==========": 0, + "Bulbasaur": 400, + "Ivysaur": 600, + "Venusaur": 1000, + + "Charmander": 400, + "Charmeleon": 600, + "Charizard": 1000, + + "Squirtle": 400, + "Wartortle": 600, + "Blastoise": 1000, + + "Pikachu": 600, + "Raichu": 1000, + + "==========Semi Rare==========": 0, + "Porygon": 200, + "Scyther": 200, + "Jynx": 200, + + "==========Uncommon==========": 0, + + "Omanyte": 150, + "Omastar": 500, + + "Seel": 300, + "Dewgong": 500, + + "Grimer": 200, + "Muk": 500, + + "Shellder": 200, + "Cloyster": 500, + + "Gastly": 200, + "Haunter": 500, + "Gengar": 1000, + + "Onix": 600, + + "Drowzee": 600, + + "Hypno": 600, + + "Vulpix": 200, + "Ninetales": 600, + + "Paras": 100, + "Parasect": 500, + + "Growlithe": 200, + "Arcanine": 700, + + "Tentacool": 200, + "Tentacruel": 500, + + "Mankey": 150, + "Primeape": 500, + + "Clefairy": 150, + "Clefable": 500, + + "Jigglypuff": 150, + "Wigglytuff": 500, + + "Venonat": 100, + "Venomoth": 500, + + "Diglett": 200, + "Dugtrio": 500, + + "Meowth": 250, + "Persian": 500, + + "Psyduck": 150, + "Golduck": 500, + + "Geodude": 100, + "Graveler": 500, + "Golem": 800, + + "Eevee": 200, + "Vaporeon": 800, + "Jolteon": 800, + "Flareon": 800, + + "Kabuto": 150, + "Kabutops": 500, + + "Magikarp": 150, + "Gyarados": 800, + + "Pinsir": 150, + + "Ponyta": 200, + "Rapidash": 500, + + "Slowpoke": 200, + "Slowbro": 500, + + "Magnemite": 250, + "Magneton": 500, + + "Krabby": 100, + "Kingler": 500, + + "Voltorb": 200, + "Electrode": 500, + + "Exeggcute": 250, + "Exeggcutor": 500, + + "Cubone": 300, + "Marowak": 800, + + "Hitmonlee": 400, + + "Hitmonchan": 400, + + "Lickitung": 500, + + "Koffing": 200, + "Weezing": 500, + + "Rhyhorn": 200, + "Rhydon": 500, + + "Chansey": 800, + + "Tangela": 300, + + "Horsea": 200, + "Seadra": 600, + + "Goldeen": 150, + "Seaking": 500, + + "Staryu": 200, + "Starmie": 800, + + + "==========T1 Evolvers==========": 0, + "Caterpie": 10, + "Metapod": 10, + "Butterfree": 500, + + "Weedle": 10, + "Kakuna": 10, + "Beedrill": 500, + + "Pidgey": 10, + "Pidgeotto": 10, + "Pidgeot": 300, + + "==========T2 Evolvers==========": 0, + "Nidoran F": 10, + "Nidorina": 10, + "Nidoqueen": 10, + + "Nidoran M": 10, + "Nidorino": 10, + "Nidoking": 10, + + "Oddish": 100, + "Gloom": 200, + "Vileplume": 600, + + "Poliwag": 200, + "Poliwhirl": 400, + "Poliwrath": 800, + + "Abra": 300, + "Kadabra": 600, + "Alakazam": 800, + + "Machop": 150, + "Machoke": 400, + "Machamp": 800, + + "Bellsprout": 100, + "Weepinbell": 400, + "Victreebel": 800, + + "==========Trash==========": 0, + + "Rattata": 10, + "Raticate": 10, + + "Spearow": 10, + "Fearow": 10, + + "Ekans": 10, + "Arbok": 10, + + "Sandshrew": 10, + "Sandslash": 10, + + "Zubat": 10, + "Golbat": 10, + + "Doduo": 10, + "Dodrio": 10 + } + } + }, + { + "type": "MoveToFort" + }, + { + "type": "FollowSpiral" + } + ], + "map_object_cache_time": 5, + "forts": { + "avoid_circles": true, + "max_circle_size": 50 + }, + "websocket_server": false, + "walk": 4.16, + "action_wait_min": 1, + "action_wait_max": 4, + "debug": false, + "test": false, + "health_record": true, + "location_cache": true, + "distance_unit": "km", + "reconnecting_timeout": 15, + "evolve_captured": "NONE", + "catch_randomize_reticle_factor": 1.0, + "catch_randomize_spin_factor": 1.0, + "catch": { + "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, + "// Example of always catching Rattata:": {}, + "// Rattata": { "always_catch" : true } + }, + "release": { + "any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}, + "// Example of always releasing Rattata:": {}, + "// Rattata": {"always_release": true}, + "// Example of keeping 3 stronger (based on CP) Pidgey:": {}, + "// Pidgey": {"keep_best_cp": 3}, + "// Example of keeping 2 stronger (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_iv": 2}, + "// Also, it is working with any": {}, + "// any": {"keep_best_iv": 3}, + "// Example of keeping the 2 strongest (based on CP) and 3 best (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_cp": 2, "keep_best_iv": 3} + }, + "vips" : { + "Any pokemon put here directly force to use Berry & Best Ball to capture, to secure the capture rate!": {}, + "any": {"catch_above_cp": 1200, "catch_above_iv": 0.9, "logic": "or" }, + "Lapras": {}, + "Moltres": {}, + "Zapdos": {}, + "Articuno": {}, + + "// S-Tier pokemons (if pokemon can be evolved into tier, list the representative)": {}, + "Mewtwo": {}, + "Dragonite": {}, + "Snorlax": {}, + "// Mew evolves to Mewtwo": {}, + "Mew": {}, + "Arcanine": {}, + "Vaporeon": {}, + "Gyarados": {}, + "Exeggutor": {}, + "Muk": {}, + "Weezing": {}, + "Flareon": {} + + } +} diff --git a/configs/config.json.path.example b/configs/config.json.path.example new file mode 100644 index 0000000000..afd1e3afeb --- /dev/null +++ b/configs/config.json.path.example @@ -0,0 +1,101 @@ +{ + "auth_service": "google", + "username": "YOUR_USERNAME", + "password": "YOUR_PASSWORD", + "location": "SOME_LOCATION", + "gmapkey": "GOOGLE_MAPS_API_KEY", + "tasks": [ + { + "type": "HandleSoftBan" + }, + { + "type": "CollectLevelUpReward" + }, + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + }, + { + "type": "TransferPokemon" + }, + { + "type": "EvolvePokemon", + "config": { + "evolve_all": "none", + "first_evolve_by": "cp", + "evolve_above_cp": 500, + "evolve_above_iv": 0.8, + "logic": "or", + "evolve_speed": 20, + "use_lucky_egg": false + } + }, + { + "type": "RecycleItems", + "config": { + "item_filter": { + "Pokeball": { "keep" : 100 }, + "Potion": { "keep" : 10 }, + "Super Potion": { "keep" : 20 }, + "Hyper Potion": { "keep" : 30 }, + "Revive": { "keep" : 30 }, + "Razz Berry": { "keep" : 100 } + } + } + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort" + }, + { + "type": "FollowPath", + "config": { + "path_mode": "loop", + "path_file": "configs/path.example.json" + } + } + ], + "map_object_cache_time": 5, + "forts": { + "avoid_circles": true, + "max_circle_size": 50 + }, + "websocket_server": false, + "walk": 4.16, + "action_wait_min": 1, + "action_wait_max": 4, + "debug": false, + "test": false, + "health_record": true, + "location_cache": true, + "distance_unit": "km", + "reconnecting_timeout": 15, + "evolve_captured": "NONE", + "catch_randomize_reticle_factor": 1.0, + "catch_randomize_spin_factor": 1.0, + "catch": { + "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, + "// Example of always catching Rattata:": {}, + "// Rattata": { "always_catch" : true } + }, + "release": { + "any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}, + "// Example of always releasing Rattata:": {}, + "// Rattata": {"always_release": true}, + "// Example of keeping 3 stronger (based on CP) Pidgey:": {}, + "// Pidgey": {"keep_best_cp": 3}, + "// Example of keeping 2 stronger (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_iv": 2}, + "// Also, it is working with any": {}, + "// any": {"keep_best_iv": 3}, + "// Example of keeping the 2 strongest (based on CP) and 3 best (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_cp": 2, "keep_best_iv": 3} + } +} diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example new file mode 100644 index 0000000000..7cad1ac066 --- /dev/null +++ b/configs/config.json.pokemon.example @@ -0,0 +1,357 @@ +{ + "auth_service": "google", + "username": "YOUR_USERNAME", + "password": "YOUR_PASSWORD", + "location": "SOME_LOCATION", + "gmapkey": "GOOGLE_MAPS_API_KEY", + "tasks": [ + { + "type": "HandleSoftBan" + }, + { + "type": "CollectLevelUpReward" + }, + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + }, + { + "type": "TransferPokemon" + }, + { + "type": "EvolvePokemon", + "config": { + "evolve_all": "none", + "first_evolve_by": "cp", + "evolve_above_cp": 500, + "evolve_above_iv": 0.8, + "logic": "or", + "evolve_speed": 20, + "use_lucky_egg": false + } + }, + { + "type": "RecycleItems", + "config": { + "item_filter": { + "Pokeball": { "keep" : 100 }, + "Potion": { "keep" : 10 }, + "Super Potion": { "keep" : 20 }, + "Hyper Potion": { "keep" : 30 }, + "Revive": { "keep" : 30 }, + "Razz Berry": { "keep" : 100 } + } + } + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort" + }, + { + "type": "MoveToFort", + "config":{ + "lure_attraction": true, + "lure_max_distance": 2000 + } + }, + { + "type": "FollowSpiral", + "config": { + "diameter": 4, + "step_size": 70 + } + } + ], + "map_object_cache_time": 5, + "forts": { + "avoid_circles": true, + "max_circle_size": 50 + }, + "websocket_server": false, + "walk": 4.16, + "action_wait_min": 1, + "action_wait_max": 4, + "debug": false, + "test": false, + "health_record": true, + "location_cache": true, + "distance_unit": "km", + "reconnecting_timeout": 15, + "evolve_captured": "NONE", + "catch_randomize_reticle_factor": 1.0, + "catch_randomize_spin_factor": 1.0, + "catch": { + "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or" }, + + "// Pokemons with example": { "always_catch": true }, + "// Gets filtered with release parameters": {}, + + "// Legendary pokemons (Goes under S-Tier)": {}, + "Lapras": { "always_catch": true }, + "Moltres": { "always_catch": true }, + "Zapdos": { "always_catch": true }, + "Articuno": { "always_catch": true }, + + "// S-Tier pokemons (if pokemon can be evolved into tier, list the representative)": {}, + "Mewtwo": { "always_catch": true }, + "Dragonite": { "always_catch": true }, + "Snorlax": { "always_catch": true }, + "// Mew evolves to Mewtwo": {}, + "Mew": { "always_catch": true }, + "Arcanine": { "always_catch": true }, + "Vaporeon": { "always_catch": true }, + "Gyarados": { "always_catch": true }, + "Exeggutor": { "always_catch": true }, + "Muk": { "always_catch": true }, + "Weezing": { "always_catch": true }, + "Flareon": { "always_catch": true }, + + "// Growlithe evolves to Arcanine": {}, + "Growlithe": { "always_catch": true }, + "// Dragonair evolves to Dragonite": {}, + "Dragonair": { "always_catch": true }, + "// Grimer evolves to Muk": {}, + "Grimer": { "always_catch": true }, + + "// Magikarp evolves to Gyarados": {}, + "Magikarp": { "always_catch": true }, + "// Exeggcute evolves to Exeggutor": {}, + "Exeggcute": { "always_catch": true }, + "// Eevee evolves to many versions, like Vaporeon, Flareon": {}, + "Eevee": { "always_catch": true }, + + "// A-Tier pokemons": {}, + "Slowbro": { "always_catch": true }, + "Victreebel": { "always_catch": true }, + "Machamp": { "always_catch": true }, + "Poliwrath": { "always_catch": true }, + "Clefable": { "always_catch": true }, + "Nidoking": { "always_catch": true }, + "Venusaur": { "always_catch": true }, + "Charizard": { "always_catch": true }, + "Golduck": { "always_catch": true }, + "Nidoqueen": { "always_catch": true }, + "Vileplume": { "always_catch": true }, + "Blastoise": { "always_catch": true }, + "Omastar": { "always_catch": true }, + "Aerodactyl": { "always_catch": true }, + "Golem": { "always_catch": true }, + "Wigglytuff": { "always_catch": true }, + "Dewgong": { "always_catch": true }, + "Ninetales": { "always_catch": true }, + "Magmar": { "always_catch": true }, + "Kabutops": { "always_catch": true }, + "Electabuzz": { "always_catch": true }, + "Starmie": { "always_catch": true }, + "Jolteon": { "always_catch": true }, + "Rapidash": { "always_catch": true }, + "Pinsir": { "always_catch": true }, + "Scyther": { "always_catch": true }, + "Tentacruel": { "always_catch": true }, + "Gengar": { "always_catch": true }, + "Hypno": { "always_catch": true }, + "Pidgeot": { "always_catch": true }, + "Rhydon": { "always_catch": true }, + "Seaking": { "always_catch": true }, + "Kangaskhan": { "always_catch": true } + }, + "release": { + "any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or" }, + + "// Legendary pokemons (Goes under S-Tier)": {}, + "Lapras": { "release_below_cp": 1041, "release_below_iv": 0.8, "logic": "and" }, + "Moltres": { "release_below_cp": 1132, "release_below_iv": 0.8, "logic": "and" }, + "Zapdos": { "release_below_cp": 1087, "release_below_iv": 0.8, "logic": "and" }, + "Articuno": { "release_below_cp": 1039, "release_below_iv": 0.8, "logic": "and" }, + + "// S-Tier pokemons (if pokemon can be evolved into tier, list the representative)": {}, + "Mewtwo": { "release_below_cp": 1447, "release_below_iv": 0.8, "logic": "and"}, + "Dragonite": { "release_below_cp": 1221, "release_below_iv": 0.8, "logic": "and" }, + "Snorlax": { "release_below_cp": 1087, "release_below_iv": 0.8, "logic": "and" }, + "// Mew evolves to Mewtwo": {}, + "Mew": { "release_below_cp": 1152, "release_below_iv": 0.8, "logic": "and" }, + "Arcanine": { "release_below_cp": 1041, "release_below_iv": 0.8, "logic": "and" }, + "Vaporeon": { "release_below_cp": 984, "release_below_iv": 0.8, "logic": "and" }, + "Gyarados": { "release_below_cp": 938, "release_below_iv": 0.8, "logic": "and" }, + "Exeggutor": { "release_below_cp": 1032, "release_below_iv": 0.8, "logic": "and" }, + "Muk": { "release_below_cp": 909, "release_below_iv": 0.8, "logic": "and" }, + "Weezing": { "release_below_cp": 784, "release_below_iv": 0.8, "logic": "and" }, + "Flareon": { "release_below_cp": 924, "release_below_iv": 0.8, "logic": "and" }, + + "// Growlithe evolves to Arcanine": {}, + "Growlithe": { "release_below_cp": 465, "release_below_iv": 0.8, "logic": "and" }, + "// Dragonair evolves to Dragonite": {}, + "Dragonair": { "release_below_cp": 609, "release_below_iv": 0.8, "logic": "and" }, + "// Grimer evolves to Muk": {}, + "Grimer": { "release_below_cp": 448, "release_below_iv": 0.8, "logic": "and" }, + "// Magikarp evolves to Gyarados": {}, + "Magikarp": { "release_below_cp": 91, "release_below_iv": 0.8, "logic": "and" }, + "// Exeggcute evolves to Exeggutor": {}, + "Exeggcute": { "release_below_cp": 384, "release_below_iv": 0.8, "logic": "and" }, + "// Eevee evolves to many versions, like Vaporeon, Flareon": {}, + "Eevee": { "release_below_cp": 376, "release_below_iv": 0.8, "logic": "and" }, + + "// A-Tier pokemons": {}, + "Slowbro": { "release_below_cp": 907, "release_below_iv": 0.8, "logic": "and" }, + "Victreebel": { "release_below_cp": 883, "release_below_iv": 0.8, "logic": "and" }, + "Machamp": { "release_below_cp": 907, "release_below_iv": 0.8, "logic": "and" }, + "Poliwrath": { "release_below_cp": 876, "release_below_iv": 0.8, "logic": "and" }, + "Clefable": { "release_below_cp": 837, "release_below_iv": 0.8, "logic": "and" }, + "Nidoking": { "release_below_cp": 864, "release_below_iv": 0.8, "logic": "and" }, + "Venusaur": { "release_below_cp": 902, "release_below_iv": 0.8, "logic": "and" }, + "Charizard": { "release_below_cp": 909, "release_below_iv": 0.8, "logic": "and" }, + "Golduck": { "release_below_cp": 832, "release_below_iv": 0.8, "logic": "and" }, + "Nidoqueen": { "release_below_cp": 868, "release_below_iv": 0.8, "logic": "and" }, + "Vileplume": { "release_below_cp": 871, "release_below_iv": 0.8, "logic": "and" }, + "Blastoise": { "release_below_cp": 888, "release_below_iv": 0.8, "logic": "and" }, + "Omastar": { "release_below_cp": 780, "release_below_iv": 0.8, "logic": "and" }, + "Aerodactyl": { "release_below_cp": 756, "release_below_iv": 0.8, "logic": "and" }, + "Golem": { "release_below_cp": 804, "release_below_iv": 0.8, "logic": "and" }, + "Wigglytuff": { "release_below_cp": 760, "release_below_iv": 0.8, "logic": "and" }, + "Dewgong": { "release_below_cp": 748, "release_below_iv": 0.8, "logic": "and" }, + "Ninetales": { "release_below_cp": 763, "release_below_iv": 0.8, "logic": "and" }, + "Magmar": { "release_below_cp": 792, "release_below_iv": 0.8, "logic": "and" }, + "Kabutops": { "release_below_cp": 744, "release_below_iv": 0.8, "logic": "and" }, + "Electabuzz": { "release_below_cp": 739, "release_below_iv": 0.8, "logic": "and" }, + "Starmie": { "release_below_cp": 763, "release_below_iv": 0.8, "logic": "and" }, + "Jolteon": { "release_below_cp": 746, "release_below_iv": 0.8, "logic": "and" }, + "Rapidash": { "release_below_cp": 768, "release_below_iv": 0.8, "logic": "and" }, + "Pinsir": { "release_below_cp": 741, "release_below_iv": 0.8, "logic": "and" }, + "Scyther": { "release_below_cp": 724, "release_below_iv": 0.8, "logic": "and" }, + "Tentacruel": { "release_below_cp": 775, "release_below_iv": 0.8, "logic": "and" }, + "Gengar": { "release_below_cp": 724, "release_below_iv": 0.8, "logic": "and" }, + "Hypno": { "release_below_cp": 763, "release_below_iv": 0.8, "logic": "and" }, + "Pidgeot": { "release_below_cp": 729, "release_below_iv": 0.8, "logic": "and" }, + "Rhydon": { "release_below_cp": 782, "release_below_iv": 0.8, "logic": "and" }, + "Seaking": { "release_below_cp": 712, "release_below_iv": 0.8, "logic": "and" }, + "Kangaskhan": { "release_below_cp": 712, "release_below_iv": 0.8, "logic": "and" }, + + "// Koffing evolves to Weezing (A-Tier)": {}, + "Koffing": { "release_below_cp": 403, "release_below_iv": 0.8, "logic": "and" }, + + "// Below is B-tier and lower pokemons": {}, + "Caterpie": { "release_below_cp": 156, "release_below_iv": 0.8, "logic": "and" }, + "Weedle": { "release_below_cp": 156, "release_below_iv": 0.8, "logic": "and" }, + "Diglett": { "release_below_cp": 158, "release_below_iv": 0.8, "logic": "and" }, + "Metapod": { "release_below_cp": 168, "release_below_iv": 0.8, "logic": "and" }, + "Kakuna": { "release_below_cp": 170, "release_below_iv": 0.8, "logic": "and" }, + "Rattata": { "release_below_cp": 204, "release_below_iv": 0.8, "logic": "and" }, + "Abra": { "release_below_cp": 208, "release_below_iv": 0.8, "logic": "and" }, + "Zubat": { "release_below_cp": 225, "release_below_iv": 0.8, "logic": "and" }, + "Chansey": { "release_below_cp": 235, "release_below_iv": 0.8, "logic": "and" }, + "Pidgey": { "release_below_cp": 237, "release_below_iv": 0.8, "logic": "and" }, + "Spearow": { "release_below_cp": 240, "release_below_iv": 0.8, "logic": "and" }, + "Meowth": { "release_below_cp": 264, "release_below_iv": 0.8, "logic": "and" }, + "Krabby": { "release_below_cp": 276, "release_below_iv": 0.8, "logic": "and" }, + "Sandshrew": { "release_below_cp": 278, "release_below_iv": 0.8, "logic": "and" }, + "Poliwag": { "release_below_cp": 278, "release_below_iv": 0.8, "logic": "and" }, + "Horsea": { "release_below_cp": 278, "release_below_iv": 0.8, "logic": "and" }, + "Gastly": { "release_below_cp": 280, "release_below_iv": 0.8, "logic": "and" }, + "Ekans": { "release_below_cp": 288, "release_below_iv": 0.8, "logic": "and" }, + "Shellder": { "release_below_cp": 288, "release_below_iv": 0.8, "logic": "and" }, + "Vulpix": { "release_below_cp": 290, "release_below_iv": 0.8, "logic": "and" }, + "Voltorb": { "release_below_cp": 292, "release_below_iv": 0.8, "logic": "and" }, + "Geodude": { "release_below_cp": 297, "release_below_iv": 0.8, "logic": "and" }, + "Doduo": { "release_below_cp": 297, "release_below_iv": 0.8, "logic": "and" }, + "Onix": { "release_below_cp": 300, "release_below_iv": 0.8, "logic": "and" }, + "Mankey": { "release_below_cp": 307, "release_below_iv": 0.8, "logic": "and" }, + "Pikachu": { "release_below_cp": 309, "release_below_iv": 0.8, "logic": "and" }, + "Magnemite": { "release_below_cp": 312, "release_below_iv": 0.8, "logic": "and" }, + "Tentacool": { "release_below_cp": 316, "release_below_iv": 0.8, "logic": "and" }, + "Paras": { "release_below_cp": 319, "release_below_iv": 0.8, "logic": "and" }, + "Jigglypuff": { "release_below_cp": 321, "release_below_iv": 0.8, "logic": "and" }, + "Ditto": { "release_below_cp": 321, "release_below_iv": 0.8, "logic": "and" }, + "Staryu": { "release_below_cp": 326, "release_below_iv": 0.8, "logic": "and" }, + "Charmander": { "release_below_cp": 333, "release_below_iv": 0.8, "logic": "and" }, + "Goldeen": { "release_below_cp": 336, "release_below_iv": 0.8, "logic": "and" }, + "Squirtle": { "release_below_cp": 352, "release_below_iv": 0.8, "logic": "and" }, + "Cubone": { "release_below_cp": 352, "release_below_iv": 0.8, "logic": "and" }, + "Venonat": { "release_below_cp": 360, "release_below_iv": 0.8, "logic": "and" }, + "Bulbasaur": { "release_below_cp": 374, "release_below_iv": 0.8, "logic": "and" }, + "Drowzee": { "release_below_cp": 374, "release_below_iv": 0.8, "logic": "and" }, + "Machop": { "release_below_cp": 381, "release_below_iv": 0.8, "logic": "and" }, + "Psyduck": { "release_below_cp": 386, "release_below_iv": 0.8, "logic": "and" }, + "Seel": { "release_below_cp": 386, "release_below_iv": 0.8, "logic": "and" }, + "Kabuto": { "release_below_cp": 386, "release_below_iv": 0.8, "logic": "and" }, + "Bellsprout": { "release_below_cp": 391, "release_below_iv": 0.8, "logic": "and" }, + "Omanyte": { "release_below_cp": 391, "release_below_iv": 0.8, "logic": "and" }, + "Kadabra": { "release_below_cp": 396, "release_below_iv": 0.8, "logic": "and" }, + "Oddish": { "release_below_cp": 400, "release_below_iv": 0.8, "logic": "and" }, + "Dugtrio": { "release_below_cp": 408, "release_below_iv": 0.8, "logic": "and" }, + "Rhyhorn": { "release_below_cp": 412, "release_below_iv": 0.8, "logic": "and" }, + "Clefairy": { "release_below_cp": 420, "release_below_iv": 0.8, "logic": "and" }, + "Slowpoke": { "release_below_cp": 424, "release_below_iv": 0.8, "logic": "and" }, + "Pidgeotto": { "release_below_cp": 427, "release_below_iv": 0.8, "logic": "and" }, + "Farfetch'd": { "release_below_cp": 441, "release_below_iv": 0.8, "logic": "and" }, + "Poliwhirl": { "release_below_cp": 468, "release_below_iv": 0.8, "logic": "and" }, + "Nidorino": { "release_below_cp": 480, "release_below_iv": 0.8, "logic": "and" }, + "Haunter": { "release_below_cp": 482, "release_below_iv": 0.8, "logic": "and" }, + "Nidorina": { "release_below_cp": 489, "release_below_iv": 0.8, "logic": "and" }, + "Graveler": { "release_below_cp": 501, "release_below_iv": 0.8, "logic": "and" }, + "Beedrill": { "release_below_cp": 504, "release_below_iv": 0.8, "logic": "and" }, + "Raticate": { "release_below_cp": 504, "release_below_iv": 0.8, "logic": "and" }, + "Butterfree": { "release_below_cp": 508, "release_below_iv": 0.8, "logic": "and" }, + "Hitmonlee": { "release_below_cp": 520, "release_below_iv": 0.8, "logic": "and" }, + "Ponyta": { "release_below_cp": 530, "release_below_iv": 0.8, "logic": "and" }, + "Hitmonchan": { "release_below_cp": 530, "release_below_iv": 0.8, "logic": "and" }, + "Charmeleon": { "release_below_cp": 544, "release_below_iv": 0.8, "logic": "and" }, + "Wartortle": { "release_below_cp": 552, "release_below_iv": 0.8, "logic": "and" }, + "Persian": { "release_below_cp": 568, "release_below_iv": 0.8, "logic": "and" }, + "Lickitung": { "release_below_cp": 568, "release_below_iv": 0.8, "logic": "and" }, + "Ivysaur": { "release_below_cp": 571, "release_below_iv": 0.8, "logic": "and" }, + "Electrode": { "release_below_cp": 576, "release_below_iv": 0.8, "logic": "and" }, + "Marowak": { "release_below_cp": 578, "release_below_iv": 0.8, "logic": "and" }, + "Gloom": { "release_below_cp": 590, "release_below_iv": 0.8, "logic": "and" }, + "Porygon": { "release_below_cp": 590, "release_below_iv": 0.8, "logic": "and" }, + "Seadra": { "release_below_cp": 597, "release_below_iv": 0.8, "logic": "and" }, + "Jynx": { "release_below_cp": 600, "release_below_iv": 0.8, "logic": "and" }, + "Weepinbell": { "release_below_cp": 602, "release_below_iv": 0.8, "logic": "and" }, + "Tangela": { "release_below_cp": 607, "release_below_iv": 0.8, "logic": "and" }, + "Fearow": { "release_below_cp": 609, "release_below_iv": 0.8, "logic": "and" }, + "Parasect": { "release_below_cp": 609, "release_below_iv": 0.8, "logic": "and" }, + "Machoke": { "release_below_cp": 614, "release_below_iv": 0.8, "logic": "and" }, + "Arbok": { "release_below_cp": 616, "release_below_iv": 0.8, "logic": "and" }, + "Sandslash": { "release_below_cp": 631, "release_below_iv": 0.8, "logic": "and" }, + "Alakazam": { "release_below_cp": 633, "release_below_iv": 0.8, "logic": "and" }, + "Kingler": { "release_below_cp": 636, "release_below_iv": 0.8, "logic": "and" }, + "Dodrio": { "release_below_cp": 640, "release_below_iv": 0.8, "logic": "and" }, + "Tauros": { "release_below_cp": 643, "release_below_iv": 0.8, "logic": "and" }, + "Primeape": { "release_below_cp": 650, "release_below_iv": 0.8, "logic": "and" }, + "Magneton": { "release_below_cp": 657, "release_below_iv": 0.8, "logic": "and" }, + "Venomoth": { "release_below_cp": 660, "release_below_iv": 0.8, "logic": "and" }, + "Golbat": { "release_below_cp": 672, "release_below_iv": 0.8, "logic": "and" }, + "Raichu": { "release_below_cp": 708, "release_below_iv": 0.8, "logic": "and" }, + "Cloyster": { "release_below_cp": 717, "release_below_iv": 0.8, "logic": "and"}, + "Mr. Mime": { "release_below_cp": 650, "release_below_iv": 0.8, "logic": "and" } + }, + "vips" : { + "Any pokemon put here directly force to use Berry & Best Ball to capture, to secure the capture rate!": {}, + "any": {"catch_above_cp": 1200, "catch_above_iv": 0.9, "logic": "or" }, + "Lapras": {}, + "Moltres": {}, + "Zapdos": {}, + "Articuno": {}, + + "// S-Tier pokemons (if pokemon can be evolved into tier, list the representative)": {}, + "Mewtwo": {}, + "Dragonite": {}, + "Snorlax": {}, + "// Mew evolves to Mewtwo": {}, + "Mew": {}, + "Arcanine": {}, + "Vaporeon": {}, + "Gyarados": {}, + "Exeggutor": {}, + "Muk": {}, + "Weezing": {}, + "Flareon": {} + + } +} diff --git a/configs/path.example.json b/configs/path.example.json new file mode 100644 index 0000000000..4936880587 --- /dev/null +++ b/configs/path.example.json @@ -0,0 +1,6 @@ +[ + {"location": "32.087504, 34.806118"}, + {"location": "Bialik 150, Ramat Gan"}, + {"location": "Ayalon Highway, Ramat Gan"}, + {"location": "32.091280, 34.795261"} +] diff --git a/data/pokemon.json b/data/pokemon.json index 795343c572..a227106841 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -1 +1 @@ -[{"Number":"001","Name":"Bulbasaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Tackle","Vine Whip"],"Weight":"6.9 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"002","Name":"Ivysaur"},{"Number":"003","Name":"Venusaur"}]},{"Number":"002","Name":"Ivysaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"13.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"003","Name":"Venusaur"}]},{"Number":"003","Name":"Venusaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"100.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"},{"Number":"002","Name":"Ivysaur"}]},{"Number":"004","Name":"Charmander","Classification":"Lizard Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Scratch"],"Weight":"8.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"005","Name":"Charmeleon"},{"Number":"006","Name":"Charizard"}]},{"Number":"005","Name":"Charmeleon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"19.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"}],"Next Evolution Requirements":{"Amount":100,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"006","Name":"Charizard"}]},{"Number":"006","Name":"Charizard","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Ember","Wing Attack"],"Weight":"90.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"},{"Number":"005","Name":"Charmeleon"}]},{"Number":"007","Name":"Squirtle","Classification":"Tiny Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Tackle","Bubble"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"008","Name":"Wartortle"},{"Number":"009","Name":"Blastoise"}]},{"Number":"008","Name":"Wartortle","Classification":"Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"22.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"}],"Next Evolution Requirements":{"Amount":100,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"009","Name":"Blastoise"}]},{"Number":"009","Name":"Blastoise","Classification":"Shellfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"85.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"},{"Number":"008","Name":"Wartortle"}]},{"Number":"010","Name":"Caterpie","Classification":"Worm Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"2.9 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"011","Name":"Metapod"},{"Number":"012","Name":"Butterfree"}]},{"Number":"011","Name":"Metapod","Classification":"Cocoon Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"9.9 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"}],"Next Evolution Requirements":{"Amount":50,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"012","Name":"Butterfree"}]},{"Number":"012","Name":"Butterfree","Classification":"Butterfly Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"32.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"},{"Number":"011","Name":"Metapod"}]},{"Number":"013","Name":"Weedle","Classification":"Hairy Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Sting"],"Weight":"3.2 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"014","Name":"Kakuna"},{"Number":"015","Name":"Beedrill"}]},{"Number":"014","Name":"Kakuna","Classification":"Cocoon Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Posion Sting"],"Weight":"10.0 kg","Height":"0.6 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"}],"Next Evolution Requirements":{"Amount":50,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"015","Name":"Beedrill"}]},{"Number":"015","Name":"Beedrill","Classification":"Poison Bee Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Jab"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"},{"Number":"014","Name":"Kakuna"}]},{"Number":"016","Name":"Pidgey","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"1.8 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"017","Name":"Pidgeotto"},{"Number":"018","Name":"Pidgeot"}]},{"Number":"017","Name":"Pidgeotto","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"30.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"}],"Next Evolution Requirements":{"Amount":50,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"018","Name":"Pidgeot"}]},{"Number":"018","Name":"Pidgeot","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Hurricane"],"Weight":"39.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"},{"Number":"017","Name":"Pidgeotto"}]},{"Number":"019","Name":"Rattata","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Body Slam","Dig","Hyper Fang"],"Weight":"3.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Rattata candies"},"Next evolution(s)":[{"Number":"020","Name":"Raticate"}]},{"Number":"020","Name":"Raticate","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Quick Attack"],"Special Attack(s)":["Dig","Hyper Beam","Hyper Fang"],"Weight":"18.5 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"019","Name":"Rattata"}]},{"Number":"021","Name":"Spearow","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"2.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Spearow candies"},"Next evolution(s)":[{"Number":"022","Name":"Fearow"}]},{"Number":"022","Name":"Fearow","Classification":"Beak Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Steel Wing"],"Weight":"38.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"021","Name":"Spearow"}]},{"Number":"023","Name":"Ekans","Classification":"Snake Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Sting"],"Weight":"6.9 kg","Height":"2.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ekans candies"},"Next evolution(s)":[{"Number":"024","Name":"Arbok"}]},{"Number":"024","Name":"Arbok","Classification":"Cobra Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Bite"],"Weight":"65.0 kg","Height":"3.5 m","Previous evolution(s)":[{"Number":"023","Name":"Ekans"}]},{"Number":"025","Name":"Pikachu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Quick Attack","Thunder Shock"],"Weight":"6.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Pikachu candies"},"Next evolution(s)":[{"Number":"026","Name":"Raichu"}]},{"Number":"026","Name":"Raichu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock","Spark"],"Weight":"30.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"025","Name":"Pikachu"}]},{"Number":"027","Name":"Sandshrew","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"12.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Sandshrew candies"},"Next evolution(s)":[{"Number":"028","Name":"Sandslash"}]},{"Number":"028","Name":"Sandslash","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"027","Name":"Sandshrew"}]},{"Number":"029","Name":"Nidoran F","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"7.0 kg","Height":"0.4 m","Next evolution(s)":[{"Number":"030","Name":"Nidorina"},{"Number":"031","Name":"Nidoqueen"}]},{"Number":"030","Name":"Nidorina","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"20.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"031","Name":"Nidoqueen"}]},{"Number":"031","Name":"Nidoqueen","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"},{"Number":"030","Name":"Nidorina"}]},{"Number":"032","Name":"Nidoran M","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Peck","Poison Sting"],"Weight":"9.0 kg","Height":"0.5 m","Next evolution(s)":[{"Number":"033","Name":"Nidorino"},{"Number":"034","Name":"Nidoking"}]},{"Number":"033","Name":"Nidorino","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"19.5 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"}],"Next Evolution Requirements":{"Amount":100,"Name":"NidoranM candies"},"Next evolution(s)":[{"Number":"034","Name":"Nidoking"}]},{"Number":"034","Name":"Nidoking","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Fury Cutter","Poison Jab"],"Weight":"62.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"},{"Number":"033","Name":"Nidorino"}]},{"Number":"035","Name":"Clefairy","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"7.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Clefairy candies"},"Next evolution(s)":[{"Number":"036","Name":"Clefable"}]},{"Number":"036","Name":"Clefable","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"40.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"035","Name":"Clefairy"}]},{"Number":"037","Name":"Vulpix","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"9.9 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Vulpi"},"Next evolution(s)":[{"Number":"038","Name":"Ninetales"}]},{"Number":"038","Name":"Ninetales","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"19.9 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"037","Name":"Vulpix"}]},{"Number":"039","Name":"Jigglypuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"5.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Jigglypuff candies"},"Next evolution(s)":[{"Number":"039","Name":"Jigglypuff"}]},{"Number":"040","Name":"Wigglytuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"12.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"040","Name":"Wigglytuff"}]},{"Number":"041","Name":"Zubat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Quick Attack"],"Weight":"7.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Zubat candies"},"Next evolution(s)":[{"Number":"042","Name":"Golbat"}]},{"Number":"042","Name":"Golbat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Wing Attack"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"041","Name":"Zubat"}]},{"Number":"043","Name":"Oddish","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"5.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"044","Name":"Gloom"},{"Number":"045","Name":"Vileplume"}]},{"Number":"044","Name":"Gloom","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"8.6 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"}],"Next Evolution Requirements":{"Amount":100,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"045","Name":"Vileplume"}]},{"Number":"045","Name":"Vileplume","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid",""],"Weight":"18.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"},{"Number":"044","Name":"Gloom"}]},{"Number":"046","Name":"Paras","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Scratch"],"Weight":"5.4 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Paras candies"},"Next evolution(s)":[{"Number":"047","Name":"Parasect"}]},{"Number":"047","Name":"Parasect","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Fury Cutter"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"046","Name":"Paras"}]},{"Number":"048","Name":"Venonat","Classification":"Insect Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Venonat candies"},"Next evolution(s)":[{"Number":"049","Name":"Venomoth"}]},{"Number":"049","Name":"Venomoth","Classification":"Poison Moth Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"12.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"048","Name":"Venonat"}]},{"Number":"050","Name":"Diglett","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"0.8 kg","Height":"0.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Diglett candies"},"Next evolution(s)":[{"Number":"051","Name":"Dugtrio"}]},{"Number":"051","Name":"Dugtrio","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Sucker Punch"],"Weight":"33.3 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"050","Name":"Diglett"}]},{"Number":"052","Name":"Meowth","Classification":"Scratch Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Scratch"],"Weight":"4.2 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Meowth candies"},"Next evolution(s)":[{"Number":"053","Name":"Persian"}]},{"Number":"053","Name":"Persian","Classification":"Classy Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Scratch"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"052","Name":"Meowth"}]},{"Number":"054","Name":"Psyduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun","Zen Headbutt"],"Weight":"19.6 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Psyduck candies"},"Next evolution(s)":[{"Number":"055","Name":"Golduck"}]},{"Number":"055","Name":"Golduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"76.6 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"054","Name":"Psyduck"}]},{"Number":"056","Name":"Mankey","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Scratch"],"Weight":"28.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Mankey candies"},"Next evolution(s)":[{"Number":"057","Name":"Primeape"}]},{"Number":"057","Name":"Primeape","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"056","Name":"Mankey"}]},{"Number":"058","Name":"Growlithe","Classification":"Puppy Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Ember"],"Weight":"19.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":50,"Name":"Growlithe candies"},"Next evolution(s)":[{"Number":"059","Name":"Arcanine"}]},{"Number":"059","Name":"Arcanine","Classification":"Legendary Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Fire Fang"],"Weight":"155.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"058","Name":"Growlithe"}]},{"Number":"060","Name":"Poliwag","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"12.4 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"061","Name":"Poliwhirl"},{"Number":"062","Name":"Poliwrath"}]},{"Number":"061","Name":"Poliwhirl","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"20.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"}],"Next Evolution Requirements":{"Amount":100,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"062","Name":"Poliwrath"}]},{"Number":"062","Name":"Poliwrath","Classification":"Tadpole Pokemon","Type I":["Water"],"Type II":["Fighting"],"Weaknesses":["Electric","Grass","Flying","Psychic","Fairy"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"54.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"},{"Number":"061","Name":"Poliwhirl"}]},{"Number":"063","Name":"Abra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Zen Headbutt",""],"Weight":"19.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":25,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"064","Name":"Kadabra"},{"Number":"065","Name":"Alakazam"}]},{"Number":"064","Name":"Kadabra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"56.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"}],"Next Evolution Requirements":{"Amount":100,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"065","Name":"Alakazam"}]},{"Number":"065","Name":"Alakazam","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"48.0 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"},{"Number":"064","Name":"Kadabra"}]},{"Number":"066","Name":"Machop","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"19.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"067","Name":"Machoke"},{"Number":"068","Name":"Machamp"}]},{"Number":"067","Name":"Machoke","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"70.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"}],"Next Evolution Requirements":{"Amount":100,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"068","Name":"Machamp"}]},{"Number":"068","Name":"Machamp","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Karate Chop"],"Weight":"130.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"},{"Number":"067","Name":"Machoke"}]},{"Number":"069","Name":"Bellsprout","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Vine Whip"],"Weight":"4.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"070","Name":"Weepinbell"},{"Number":"071","Name":"Victreebel"}]},{"Number":"070","Name":"Weepinbell","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"6.4 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"071","Name":"Victreebel"}]},{"Number":"071","Name":"Victreebel","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"15.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"},{"Number":"070","Name":"Weepinbell"}]},{"Number":"072","Name":"Tentacool","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Bubble","Poison Sting"],"Weight":"45.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Tentacool candies"},"Next evolution(s)":[{"Number":"073","Name":"Tentacruel"}]},{"Number":"073","Name":"Tentacruel","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Jab"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"072","Name":"Tentacool"}]},{"Number":"074","Name":"Geodude","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"20.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"075","Name":"Graveler"},{"Number":"076","Name":"Golem"}]},{"Number":"075","Name":"Graveler","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"105.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"}],"Next Evolution Requirements":{"Amount":100,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"076","Name":"Golem"}]},{"Number":"076","Name":"Golem","Classification":"Megaton Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"300.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"},{"Number":"075","Name":"Graveler"}]},{"Number":"077","Name":"Ponyta","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Tackle"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ponyta candies"},"Next evolution(s)":[{"Number":"078","Name":"Rapidash"}]},{"Number":"078","Name":"Rapidash","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Low Kick"],"Weight":"95.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"077","Name":"Ponyta"}]},{"Number":"079","Name":"Slowpoke","Classification":"Dopey Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"36.0 kg","Height":"1.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Slowpoke candies"},"Next evolution(s)":[{"Number":"080","Name":"Slowbro"}]},{"Number":"080","Name":"Slowbro","Classification":"Hermit Crab Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"78.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"079","Name":"Slowpoke"}]},{"Number":"081","Name":"Magnemite","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"6.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Magnemite candies"},"Next evolution(s)":[{"Number":"082","Name":"Magneton"}]},{"Number":"082","Name":"Magneton","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"60.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"081","Name":"Magnemite"}]},{"Number":"083","Name":"Farfetch'd","Classification":"Wild Duck Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"15.0 kg","Height":"0.8 m"},{"Number":"084","Name":"Doduo","Classification":"Twin Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"39.2 kg","Height":"1.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Doduo candies"},"Next evolution(s)":[{"Number":"085","Name":"Dodrio"}]},{"Number":"085","Name":"Dodrio","Classification":"Triple Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Feint Attack","Steel Wing"],"Weight":"85.2 kg","Height":"1.8 m","Previous evolution(s)":[{"Number":"084","Name":"Doduo"}]},{"Number":"086","Name":"Seel","Classification":"Sea Lion Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Water Gun"],"Weight":"90.0 kg","Height":"1.1 m","Next Evolution Requirements":{"Amount":50,"Name":"Seel candies"},"Next evolution(s)":[{"Number":"087","Name":"Dewgong"}]},{"Number":"087","Name":"Dewgong","Classification":"Sea Lion Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"120.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"086","Name":"Seel"}]},{"Number":"088","Name":"Grimer","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Mud Slap"],"Weight":"30.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Grimer candies"},"Next evolution(s)":[{"Number":"089","Name":"Muk"}]},{"Number":"089","Name":"Muk","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Poison Jab",""],"Weight":"30.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"088","Name":"Grimer"}]},{"Number":"090","Name":"Shellder","Classification":"Bivalve Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Tackle"],"Weight":"4.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Shellder candies"},"Next evolution(s)":[{"Number":"091","Name":"Cloyster"}]},{"Number":"091","Name":"Cloyster","Classification":"Bivalve Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"132.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"090","Name":"Shellder"}]},{"Number":"092","Name":"Gastly","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Sucker Punch"],"Weight":"0.1 kg","Height":"1.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"093","Name":"Haunter"},{"Number":"094","Name":"Gengar"}]},{"Number":"093","Name":"Haunter","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Shadow Claw"],"Weight":"0.1 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"}],"Next Evolution Requirements":{"Amount":100,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"094","Name":"Gengar"}]},{"Number":"094","Name":"Gengar","Classification":"Shadow Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Shadow Claw","Sucker Punch"],"Weight":"40.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"},{"Number":"093","Name":"Haunter"}]},{"Number":"095","Name":"Onix","Classification":"Rock Snake Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"210.0 kg","Height":"8.8 m"},{"Number":"096","Name":"Drowzee","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Pound"],"Weight":"32.4 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Drowzee candies"},"Next evolution(s)":[{"Number":"097","Name":"Hypno"}]},{"Number":"097","Name":"Hypno","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"75.6 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"096","Name":"Drowzee"}]},{"Number":"098","Name":"Krabby","Classification":"River Crab Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Krabby candies"},"Next evolution(s)":[{"Number":"099","Name":"Kingler"}]},{"Number":"099","Name":"Kingler","Classification":"Pincer Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"098","Name":"Krabby"}]},{"Number":"100","Name":"Voltorb","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark","Tackle"],"Weight":"10.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Voltorb candies"},"Next evolution(s)":[{"Number":"101","Name":"Electrode"}]},{"Number":"101","Name":"Electrode","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark",""],"Weight":"66.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"100","Name":"Voltorb"}]},{"Number":"102","Name":"Exeggcute","Classification":"Egg Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion",""],"Weight":"2.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"E"},"Next evolution(s)":[{"Number":"103","Name":"Exeggutor"}]},{"Number":"103","Name":"Exeggutor","Classification":"Coconut Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"120.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"102","Name":"Exeggcute"}]},{"Number":"104","Name":"Cubone","Classification":"Lonely Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Cubone candies"},"Next evolution(s)":[{"Number":"105","Name":"Marowak"}]},{"Number":"105","Name":"Marowak","Classification":"Bone Keeper Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"45.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"104","Name":"Cubone"}]},{"Number":"106","Name":"Hitmonlee","Classification":"Kicking Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Low Kick","Rock Smash"],"Weight":"49.8 kg","Height":"1.5 m","Next evolution(s)":[{"Number":"107","Name":"Hitmonchan"}]},{"Number":"107","Name":"Hitmonchan","Classification":"Punching Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Rock Smash"],"Weight":"50.2 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"106","Name":"Hitmonlee"}]},{"Number":"108","Name":"Lickitung","Classification":"Licking Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"65.5 kg","Height":"1.2 m"},{"Number":"109","Name":"Koffing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"1.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Koffing candies"},"Next evolution(s)":[{"Number":"110","Name":"Weezing"}]},{"Number":"110","Name":"Weezing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"9.5 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"109","Name":"Koffing"}]},{"Number":"111","Name":"Rhyhorn","Classification":"Spikes Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"115.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Rhyhorn candies"},"Next evolution(s)":[{"Number":"112","Name":"Rhydon"}]},{"Number":"112","Name":"Rhydon","Classification":"Drill Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"120.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"111","Name":"Rhyhorn"}]},{"Number":"113","Name":"Chansey","Classification":"Egg Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"34.6 kg","Height":"1.1 m"},{"Number":"114","Name":"Tangela","Classification":"Vine Pokemon","Type I":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug"],"Fast Attack(s)":["Vine Whip",""],"Weight":"35.0 kg","Height":"1.0 m"},{"Number":"115","Name":"Kangaskhan","Classification":"Parent Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Low Kick",""],"Weight":"80.0 kg","Height":"2.2 m"},{"Number":"116","Name":"Horsea","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Water Gun"],"Weight":"8.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Horsea candies"},"Next evolution(s)":[{"Number":"117","Name":"Seadra"}]},{"Number":"117","Name":"Seadra","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Dragon Breath","Water Gun"],"Weight":"25.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"116","Name":"Horsea"}]},{"Number":"118","Name":"Goldeen","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Mud Shot"],"Weight":"15.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Goldeen candies"},"Next evolution(s)":[{"Number":"119","Name":"Seaking"}]},{"Number":"119","Name":"Seaking","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Poison Jab"],"Weight":"39.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"118","Name":"Goldeen"}]},{"Number":"120","Name":"Staryu","Classification":"Starshape Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"34.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Staryu candies"},"Next evolution(s)":[{"Number":"120","Name":"Staryu"}]},{"Number":"121","Name":"Starmie","Classification":"Mysterious Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"80.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"121","Name":"Starmie"}]},{"Number":"122","Name":"Mr. Mime","Classification":"Barrier Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"54.5 kg","Height":"1.3 m"},{"Number":"123","Name":"Scyther","Classification":"Mantis Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Steel Wing"],"Weight":"56.0 kg","Height":"1.5 m"},{"Number":"124","Name":"Jynx","Classification":"Humanshape Pokemon","Type I":["Ice"],"Type II":["Psychic"],"Weaknesses":["Fire","Bug","Rock","Ghost","Dark","Steel"],"Fast Attack(s)":["Frost Breath","Pound"],"Weight":"40.6 kg","Height":"1.4 m"},{"Number":"125","Name":"Electabuzz","Classification":"Electric Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Low Kick","Thunder Shock"],"Weight":"30.0 kg","Height":"1.1 m"},{"Number":"126","Name":"Magmar","Classification":"Spitfire Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Karate Chop"],"Weight":"44.5 kg","Height":"1.3 m"},{"Number":"127","Name":"Pinsir","Classification":"Stagbeetle Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Rock Smash"],"Weight":"55.0 kg","Height":"1.5 m"},{"Number":"128","Name":"Tauros","Classification":"Wild Bull Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Tackle","Zen Headbutt"],"Weight":"88.4 kg","Height":"1.4 m"},{"Number":"129","Name":"Magikarp","Classification":"Fish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Splash",""],"Weight":"10.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":400,"Name":"Magikarp candies"},"Next evolution(s)":[{"Number":"130","Name":"Gyarados"}]},{"Number":"130","Name":"Gyarados","Classification":"Atrocious Pokemon","Type I":["Water"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Bite","Dragon Breath"],"Weight":"235.0 kg","Height":"6.5 m","Previous evolution(s)":[{"Number":"129","Name":"Magikarp"}]},{"Number":"131","Name":"Lapras","Classification":"Transport Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"220.0 kg","Height":"2.5 m"},{"Number":"132","Name":"Ditto","Classification":"Transform Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.3 m"},{"Number":"133","Name":"Eevee","Classification":"Evolution Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"6.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Eevee candies"},"Next evolution(s)":[{"Number":"134","Name":"Vaporeon"},{"Number":"135","Name":"Jolteon"},{"Number":"136","Name":"Flareon"}]},{"Number":"134","Name":"Vaporeon","Classification":"Bubble Jet Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun",""],"Weight":"29.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"135","Name":"Jolteon","Classification":"Lightning Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock",""],"Weight":"24.5 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"136","Name":"Flareon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"25.0 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"137","Name":"Porygon","Classification":"Virtual Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"36.5 kg","Height":"0.8 m"},{"Number":"138","Name":"Omanyte","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Water Gun",""],"Weight":"7.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Omanyte candies"},"Next evolution(s)":[{"Number":"139","Name":"Omastar"}]},{"Number":"139","Name":"Omastar","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Rock Throw","Water Gun"],"Weight":"35.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"138","Name":"Omanyte"}]},{"Number":"140","Name":"Kabuto","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"11.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Kabuto candies"},"Next evolution(s)":[{"Number":"141","Name":"Kabutops"}]},{"Number":"141","Name":"Kabutops","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Fury Cutter","Mud Shot"],"Weight":"40.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"140","Name":"Kabuto"}]},{"Number":"142","Name":"Aerodactyl","Classification":"Fossil Pokemon","Type I":["Rock"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Ice","Rock","Steel"],"Fast Attack(s)":["Bite","Steel Wing"],"Weight":"59.0 kg","Height":"1.8 m"},{"Number":"143","Name":"Snorlax","Classification":"Sleeping Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"460.0 kg","Height":"2.1 m"},{"Number":"144","Name":"Articuno","Classification":"Freeze Pokemon","Type I":["Ice"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Rock","Steel"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"55.4 kg","Height":"1.7 m"},{"Number":"145","Name":"Zapdos","Classification":"Electric Pokemon","Type I":["Electric"],"Type II":["Flying"],"Weaknesses":["Ice","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"52.6 kg","Height":"1.6 m"},{"Number":"146","Name":"Moltres","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"60.0 kg","Height":"2.0 m"},{"Number":"147","Name":"Dratini","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"3.3 kg","Height":"1.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Dratini candies"}},{"Number":"148","Name":"Dragonair","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"16.5 kg","Height":"4.0 m","Next Evolution Requirements":{"Amount":100,"Name":"Dratini candies"},"Next evolution(s)":[{"Number":"149","Name":"Dragonite"}]},{"Number":"149","Name":"Dragonite","Classification":"Dragon Pokemon","Type I":["Dragon"],"Type II":["Flying"],"Weaknesses":["Ice","Rock","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath","Steel Wing"],"Weight":"210.0 kg","Height":"2.2 m","Previous evolution(s)":[{"Number":"148","Name":"Dragonair"}]},{"Number":"150","Name":"Mewtwo","Classification":"Genetic Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"122.0 kg","Height":"2.0 m"},{"Number":"151","Name":"Mew","Classification":"New Species Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.4 m"}] \ No newline at end of file +[{"Number":"001","Name":"Bulbasaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Tackle","Vine Whip"],"Weight":"6.9 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"002","Name":"Ivysaur"},{"Number":"003","Name":"Venusaur"}]},{"Number":"002","Name":"Ivysaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"13.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"003","Name":"Venusaur"}]},{"Number":"003","Name":"Venusaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"100.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"},{"Number":"002","Name":"Ivysaur"}]},{"Number":"004","Name":"Charmander","Classification":"Lizard Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Scratch"],"Weight":"8.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"005","Name":"Charmeleon"},{"Number":"006","Name":"Charizard"}]},{"Number":"005","Name":"Charmeleon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"19.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"}],"Next Evolution Requirements":{"Amount":100,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"006","Name":"Charizard"}]},{"Number":"006","Name":"Charizard","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Ember","Wing Attack"],"Weight":"90.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"},{"Number":"005","Name":"Charmeleon"}]},{"Number":"007","Name":"Squirtle","Classification":"Tiny Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Tackle","Bubble"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"008","Name":"Wartortle"},{"Number":"009","Name":"Blastoise"}]},{"Number":"008","Name":"Wartortle","Classification":"Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"22.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"}],"Next Evolution Requirements":{"Amount":100,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"009","Name":"Blastoise"}]},{"Number":"009","Name":"Blastoise","Classification":"Shellfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"85.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"},{"Number":"008","Name":"Wartortle"}]},{"Number":"010","Name":"Caterpie","Classification":"Worm Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"2.9 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"011","Name":"Metapod"},{"Number":"012","Name":"Butterfree"}]},{"Number":"011","Name":"Metapod","Classification":"Cocoon Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"9.9 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"}],"Next Evolution Requirements":{"Amount":50,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"012","Name":"Butterfree"}]},{"Number":"012","Name":"Butterfree","Classification":"Butterfly Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"32.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"},{"Number":"011","Name":"Metapod"}]},{"Number":"013","Name":"Weedle","Classification":"Hairy Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Sting"],"Weight":"3.2 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"014","Name":"Kakuna"},{"Number":"015","Name":"Beedrill"}]},{"Number":"014","Name":"Kakuna","Classification":"Cocoon Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Posion Sting"],"Weight":"10.0 kg","Height":"0.6 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"}],"Next Evolution Requirements":{"Amount":50,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"015","Name":"Beedrill"}]},{"Number":"015","Name":"Beedrill","Classification":"Poison Bee Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Jab"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"},{"Number":"014","Name":"Kakuna"}]},{"Number":"016","Name":"Pidgey","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"1.8 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"017","Name":"Pidgeotto"},{"Number":"018","Name":"Pidgeot"}]},{"Number":"017","Name":"Pidgeotto","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"30.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"}],"Next Evolution Requirements":{"Amount":50,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"018","Name":"Pidgeot"}]},{"Number":"018","Name":"Pidgeot","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Hurricane"],"Weight":"39.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"},{"Number":"017","Name":"Pidgeotto"}]},{"Number":"019","Name":"Rattata","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Body Slam","Dig","Hyper Fang"],"Weight":"3.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Rattata candies"},"Next evolution(s)":[{"Number":"020","Name":"Raticate"}]},{"Number":"020","Name":"Raticate","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Quick Attack"],"Special Attack(s)":["Dig","Hyper Beam","Hyper Fang"],"Weight":"18.5 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"019","Name":"Rattata"}]},{"Number":"021","Name":"Spearow","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"2.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Spearow candies"},"Next evolution(s)":[{"Number":"022","Name":"Fearow"}]},{"Number":"022","Name":"Fearow","Classification":"Beak Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Steel Wing"],"Weight":"38.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"021","Name":"Spearow"}]},{"Number":"023","Name":"Ekans","Classification":"Snake Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Sting"],"Weight":"6.9 kg","Height":"2.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ekans candies"},"Next evolution(s)":[{"Number":"024","Name":"Arbok"}]},{"Number":"024","Name":"Arbok","Classification":"Cobra Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Bite"],"Weight":"65.0 kg","Height":"3.5 m","Previous evolution(s)":[{"Number":"023","Name":"Ekans"}]},{"Number":"025","Name":"Pikachu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Quick Attack","Thunder Shock"],"Weight":"6.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Pikachu candies"},"Next evolution(s)":[{"Number":"026","Name":"Raichu"}]},{"Number":"026","Name":"Raichu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock","Spark"],"Weight":"30.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"025","Name":"Pikachu"}]},{"Number":"027","Name":"Sandshrew","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"12.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Sandshrew candies"},"Next evolution(s)":[{"Number":"028","Name":"Sandslash"}]},{"Number":"028","Name":"Sandslash","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"027","Name":"Sandshrew"}]},{"Number":"029","Name":"Nidoran F","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"7.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"030","Name":"Nidorina"},{"Number":"031","Name":"Nidoqueen"}]},{"Number":"030","Name":"Nidorina","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"20.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"031","Name":"Nidoqueen"}]},{"Number":"031","Name":"Nidoqueen","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"},{"Number":"030","Name":"Nidorina"}]},{"Number":"032","Name":"Nidoran M","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Peck","Poison Sting"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"033","Name":"Nidorino"},{"Number":"034","Name":"Nidoking"}]},{"Number":"033","Name":"Nidorino","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"19.5 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"034","Name":"Nidoking"}]},{"Number":"034","Name":"Nidoking","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Fury Cutter","Poison Jab"],"Weight":"62.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"},{"Number":"033","Name":"Nidorino"}]},{"Number":"035","Name":"Clefairy","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"7.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Clefairy candies"},"Next evolution(s)":[{"Number":"036","Name":"Clefable"}]},{"Number":"036","Name":"Clefable","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"40.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"035","Name":"Clefairy"}]},{"Number":"037","Name":"Vulpix","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"9.9 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Vulpix candies"},"Next evolution(s)":[{"Number":"038","Name":"Ninetales"}]},{"Number":"038","Name":"Ninetales","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"19.9 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"037","Name":"Vulpix"}]},{"Number":"039","Name":"Jigglypuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"5.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Jigglypuff candies"},"Next evolution(s)":[{"Number":"039","Name":"Jigglypuff"}]},{"Number":"040","Name":"Wigglytuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"12.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"040","Name":"Wigglytuff"}]},{"Number":"041","Name":"Zubat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Quick Attack"],"Weight":"7.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Zubat candies"},"Next evolution(s)":[{"Number":"042","Name":"Golbat"}]},{"Number":"042","Name":"Golbat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Wing Attack"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"041","Name":"Zubat"}]},{"Number":"043","Name":"Oddish","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"5.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"044","Name":"Gloom"},{"Number":"045","Name":"Vileplume"}]},{"Number":"044","Name":"Gloom","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"8.6 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"}],"Next Evolution Requirements":{"Amount":100,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"045","Name":"Vileplume"}]},{"Number":"045","Name":"Vileplume","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid",""],"Weight":"18.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"},{"Number":"044","Name":"Gloom"}]},{"Number":"046","Name":"Paras","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Scratch"],"Weight":"5.4 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Paras candies"},"Next evolution(s)":[{"Number":"047","Name":"Parasect"}]},{"Number":"047","Name":"Parasect","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Fury Cutter"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"046","Name":"Paras"}]},{"Number":"048","Name":"Venonat","Classification":"Insect Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Venonat candies"},"Next evolution(s)":[{"Number":"049","Name":"Venomoth"}]},{"Number":"049","Name":"Venomoth","Classification":"Poison Moth Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"12.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"048","Name":"Venonat"}]},{"Number":"050","Name":"Diglett","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"0.8 kg","Height":"0.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Diglett candies"},"Next evolution(s)":[{"Number":"051","Name":"Dugtrio"}]},{"Number":"051","Name":"Dugtrio","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Sucker Punch"],"Weight":"33.3 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"050","Name":"Diglett"}]},{"Number":"052","Name":"Meowth","Classification":"Scratch Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Scratch"],"Weight":"4.2 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Meowth candies"},"Next evolution(s)":[{"Number":"053","Name":"Persian"}]},{"Number":"053","Name":"Persian","Classification":"Classy Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Scratch"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"052","Name":"Meowth"}]},{"Number":"054","Name":"Psyduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun","Zen Headbutt"],"Weight":"19.6 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Psyduck candies"},"Next evolution(s)":[{"Number":"055","Name":"Golduck"}]},{"Number":"055","Name":"Golduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"76.6 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"054","Name":"Psyduck"}]},{"Number":"056","Name":"Mankey","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Scratch"],"Weight":"28.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Mankey candies"},"Next evolution(s)":[{"Number":"057","Name":"Primeape"}]},{"Number":"057","Name":"Primeape","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"056","Name":"Mankey"}]},{"Number":"058","Name":"Growlithe","Classification":"Puppy Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Ember"],"Weight":"19.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":50,"Name":"Growlithe candies"},"Next evolution(s)":[{"Number":"059","Name":"Arcanine"}]},{"Number":"059","Name":"Arcanine","Classification":"Legendary Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Fire Fang"],"Weight":"155.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"058","Name":"Growlithe"}]},{"Number":"060","Name":"Poliwag","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"12.4 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"061","Name":"Poliwhirl"},{"Number":"062","Name":"Poliwrath"}]},{"Number":"061","Name":"Poliwhirl","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"20.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"}],"Next Evolution Requirements":{"Amount":100,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"062","Name":"Poliwrath"}]},{"Number":"062","Name":"Poliwrath","Classification":"Tadpole Pokemon","Type I":["Water"],"Type II":["Fighting"],"Weaknesses":["Electric","Grass","Flying","Psychic","Fairy"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"54.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"},{"Number":"061","Name":"Poliwhirl"}]},{"Number":"063","Name":"Abra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Zen Headbutt",""],"Weight":"19.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":25,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"064","Name":"Kadabra"},{"Number":"065","Name":"Alakazam"}]},{"Number":"064","Name":"Kadabra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"56.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"}],"Next Evolution Requirements":{"Amount":100,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"065","Name":"Alakazam"}]},{"Number":"065","Name":"Alakazam","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"48.0 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"},{"Number":"064","Name":"Kadabra"}]},{"Number":"066","Name":"Machop","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"19.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"067","Name":"Machoke"},{"Number":"068","Name":"Machamp"}]},{"Number":"067","Name":"Machoke","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"70.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"}],"Next Evolution Requirements":{"Amount":100,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"068","Name":"Machamp"}]},{"Number":"068","Name":"Machamp","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Karate Chop"],"Weight":"130.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"},{"Number":"067","Name":"Machoke"}]},{"Number":"069","Name":"Bellsprout","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Vine Whip"],"Weight":"4.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"070","Name":"Weepinbell"},{"Number":"071","Name":"Victreebel"}]},{"Number":"070","Name":"Weepinbell","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"6.4 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"071","Name":"Victreebel"}]},{"Number":"071","Name":"Victreebel","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"15.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"},{"Number":"070","Name":"Weepinbell"}]},{"Number":"072","Name":"Tentacool","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Bubble","Poison Sting"],"Weight":"45.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Tentacool candies"},"Next evolution(s)":[{"Number":"073","Name":"Tentacruel"}]},{"Number":"073","Name":"Tentacruel","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Jab"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"072","Name":"Tentacool"}]},{"Number":"074","Name":"Geodude","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"20.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"075","Name":"Graveler"},{"Number":"076","Name":"Golem"}]},{"Number":"075","Name":"Graveler","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"105.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"}],"Next Evolution Requirements":{"Amount":100,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"076","Name":"Golem"}]},{"Number":"076","Name":"Golem","Classification":"Megaton Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"300.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"},{"Number":"075","Name":"Graveler"}]},{"Number":"077","Name":"Ponyta","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Tackle"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ponyta candies"},"Next evolution(s)":[{"Number":"078","Name":"Rapidash"}]},{"Number":"078","Name":"Rapidash","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Low Kick"],"Weight":"95.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"077","Name":"Ponyta"}]},{"Number":"079","Name":"Slowpoke","Classification":"Dopey Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"36.0 kg","Height":"1.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Slowpoke candies"},"Next evolution(s)":[{"Number":"080","Name":"Slowbro"}]},{"Number":"080","Name":"Slowbro","Classification":"Hermit Crab Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"78.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"079","Name":"Slowpoke"}]},{"Number":"081","Name":"Magnemite","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"6.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Magnemite candies"},"Next evolution(s)":[{"Number":"082","Name":"Magneton"}]},{"Number":"082","Name":"Magneton","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"60.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"081","Name":"Magnemite"}]},{"Number":"083","Name":"Farfetch'd","Classification":"Wild Duck Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"15.0 kg","Height":"0.8 m"},{"Number":"084","Name":"Doduo","Classification":"Twin Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"39.2 kg","Height":"1.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Doduo candies"},"Next evolution(s)":[{"Number":"085","Name":"Dodrio"}]},{"Number":"085","Name":"Dodrio","Classification":"Triple Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Feint Attack","Steel Wing"],"Weight":"85.2 kg","Height":"1.8 m","Previous evolution(s)":[{"Number":"084","Name":"Doduo"}]},{"Number":"086","Name":"Seel","Classification":"Sea Lion Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Water Gun"],"Weight":"90.0 kg","Height":"1.1 m","Next Evolution Requirements":{"Amount":50,"Name":"Seel candies"},"Next evolution(s)":[{"Number":"087","Name":"Dewgong"}]},{"Number":"087","Name":"Dewgong","Classification":"Sea Lion Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"120.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"086","Name":"Seel"}]},{"Number":"088","Name":"Grimer","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Mud Slap"],"Weight":"30.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Grimer candies"},"Next evolution(s)":[{"Number":"089","Name":"Muk"}]},{"Number":"089","Name":"Muk","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Poison Jab",""],"Weight":"30.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"088","Name":"Grimer"}]},{"Number":"090","Name":"Shellder","Classification":"Bivalve Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Tackle"],"Weight":"4.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Shellder candies"},"Next evolution(s)":[{"Number":"091","Name":"Cloyster"}]},{"Number":"091","Name":"Cloyster","Classification":"Bivalve Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"132.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"090","Name":"Shellder"}]},{"Number":"092","Name":"Gastly","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Sucker Punch"],"Weight":"0.1 kg","Height":"1.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"093","Name":"Haunter"},{"Number":"094","Name":"Gengar"}]},{"Number":"093","Name":"Haunter","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Shadow Claw"],"Weight":"0.1 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"}],"Next Evolution Requirements":{"Amount":100,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"094","Name":"Gengar"}]},{"Number":"094","Name":"Gengar","Classification":"Shadow Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Shadow Claw","Sucker Punch"],"Weight":"40.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"},{"Number":"093","Name":"Haunter"}]},{"Number":"095","Name":"Onix","Classification":"Rock Snake Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"210.0 kg","Height":"8.8 m"},{"Number":"096","Name":"Drowzee","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Pound"],"Weight":"32.4 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Drowzee candies"},"Next evolution(s)":[{"Number":"097","Name":"Hypno"}]},{"Number":"097","Name":"Hypno","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"75.6 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"096","Name":"Drowzee"}]},{"Number":"098","Name":"Krabby","Classification":"River Crab Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Krabby candies"},"Next evolution(s)":[{"Number":"099","Name":"Kingler"}]},{"Number":"099","Name":"Kingler","Classification":"Pincer Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"098","Name":"Krabby"}]},{"Number":"100","Name":"Voltorb","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark","Tackle"],"Weight":"10.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Voltorb candies"},"Next evolution(s)":[{"Number":"101","Name":"Electrode"}]},{"Number":"101","Name":"Electrode","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark",""],"Weight":"66.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"100","Name":"Voltorb"}]},{"Number":"102","Name":"Exeggcute","Classification":"Egg Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion",""],"Weight":"2.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Exeggcute candies"},"Next evolution(s)":[{"Number":"103","Name":"Exeggutor"}]},{"Number":"103","Name":"Exeggutor","Classification":"Coconut Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"120.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"102","Name":"Exeggcute"}]},{"Number":"104","Name":"Cubone","Classification":"Lonely Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Cubone candies"},"Next evolution(s)":[{"Number":"105","Name":"Marowak"}]},{"Number":"105","Name":"Marowak","Classification":"Bone Keeper Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"45.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"104","Name":"Cubone"}]},{"Number":"106","Name":"Hitmonlee","Classification":"Kicking Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Low Kick","Rock Smash"],"Weight":"49.8 kg","Height":"1.5 m","Next evolution(s)":[{"Number":"107","Name":"Hitmonchan"}]},{"Number":"107","Name":"Hitmonchan","Classification":"Punching Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Rock Smash"],"Weight":"50.2 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"106","Name":"Hitmonlee"}]},{"Number":"108","Name":"Lickitung","Classification":"Licking Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"65.5 kg","Height":"1.2 m"},{"Number":"109","Name":"Koffing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"1.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Koffing candies"},"Next evolution(s)":[{"Number":"110","Name":"Weezing"}]},{"Number":"110","Name":"Weezing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"9.5 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"109","Name":"Koffing"}]},{"Number":"111","Name":"Rhyhorn","Classification":"Spikes Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"115.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Rhyhorn candies"},"Next evolution(s)":[{"Number":"112","Name":"Rhydon"}]},{"Number":"112","Name":"Rhydon","Classification":"Drill Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"120.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"111","Name":"Rhyhorn"}]},{"Number":"113","Name":"Chansey","Classification":"Egg Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"34.6 kg","Height":"1.1 m"},{"Number":"114","Name":"Tangela","Classification":"Vine Pokemon","Type I":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug"],"Fast Attack(s)":["Vine Whip",""],"Weight":"35.0 kg","Height":"1.0 m"},{"Number":"115","Name":"Kangaskhan","Classification":"Parent Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Low Kick",""],"Weight":"80.0 kg","Height":"2.2 m"},{"Number":"116","Name":"Horsea","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Water Gun"],"Weight":"8.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Horsea candies"},"Next evolution(s)":[{"Number":"117","Name":"Seadra"}]},{"Number":"117","Name":"Seadra","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Dragon Breath","Water Gun"],"Weight":"25.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"116","Name":"Horsea"}]},{"Number":"118","Name":"Goldeen","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Mud Shot"],"Weight":"15.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Goldeen candies"},"Next evolution(s)":[{"Number":"119","Name":"Seaking"}]},{"Number":"119","Name":"Seaking","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Poison Jab"],"Weight":"39.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"118","Name":"Goldeen"}]},{"Number":"120","Name":"Staryu","Classification":"Starshape Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"34.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Staryu candies"},"Next evolution(s)":[{"Number":"120","Name":"Staryu"}]},{"Number":"121","Name":"Starmie","Classification":"Mysterious Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"80.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"121","Name":"Starmie"}]},{"Number":"122","Name":"Mr. Mime","Classification":"Barrier Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"54.5 kg","Height":"1.3 m"},{"Number":"123","Name":"Scyther","Classification":"Mantis Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Steel Wing"],"Weight":"56.0 kg","Height":"1.5 m"},{"Number":"124","Name":"Jynx","Classification":"Humanshape Pokemon","Type I":["Ice"],"Type II":["Psychic"],"Weaknesses":["Fire","Bug","Rock","Ghost","Dark","Steel"],"Fast Attack(s)":["Frost Breath","Pound"],"Weight":"40.6 kg","Height":"1.4 m"},{"Number":"125","Name":"Electabuzz","Classification":"Electric Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Low Kick","Thunder Shock"],"Weight":"30.0 kg","Height":"1.1 m"},{"Number":"126","Name":"Magmar","Classification":"Spitfire Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Karate Chop"],"Weight":"44.5 kg","Height":"1.3 m"},{"Number":"127","Name":"Pinsir","Classification":"Stagbeetle Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Rock Smash"],"Weight":"55.0 kg","Height":"1.5 m"},{"Number":"128","Name":"Tauros","Classification":"Wild Bull Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Tackle","Zen Headbutt"],"Weight":"88.4 kg","Height":"1.4 m"},{"Number":"129","Name":"Magikarp","Classification":"Fish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Splash",""],"Weight":"10.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":400,"Name":"Magikarp candies"},"Next evolution(s)":[{"Number":"130","Name":"Gyarados"}]},{"Number":"130","Name":"Gyarados","Classification":"Atrocious Pokemon","Type I":["Water"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Bite","Dragon Breath"],"Weight":"235.0 kg","Height":"6.5 m","Previous evolution(s)":[{"Number":"129","Name":"Magikarp"}]},{"Number":"131","Name":"Lapras","Classification":"Transport Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"220.0 kg","Height":"2.5 m"},{"Number":"132","Name":"Ditto","Classification":"Transform Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.3 m"},{"Number":"133","Name":"Eevee","Classification":"Evolution Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"6.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Eevee candies"},"Next evolution(s)":[{"Number":"134","Name":"Vaporeon"},{"Number":"135","Name":"Jolteon"},{"Number":"136","Name":"Flareon"}]},{"Number":"134","Name":"Vaporeon","Classification":"Bubble Jet Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun",""],"Weight":"29.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"135","Name":"Jolteon","Classification":"Lightning Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock",""],"Weight":"24.5 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"136","Name":"Flareon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"25.0 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"137","Name":"Porygon","Classification":"Virtual Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"36.5 kg","Height":"0.8 m"},{"Number":"138","Name":"Omanyte","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Water Gun",""],"Weight":"7.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Omanyte candies"},"Next evolution(s)":[{"Number":"139","Name":"Omastar"}]},{"Number":"139","Name":"Omastar","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Rock Throw","Water Gun"],"Weight":"35.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"138","Name":"Omanyte"}]},{"Number":"140","Name":"Kabuto","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"11.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Kabuto candies"},"Next evolution(s)":[{"Number":"141","Name":"Kabutops"}]},{"Number":"141","Name":"Kabutops","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Fury Cutter","Mud Shot"],"Weight":"40.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"140","Name":"Kabuto"}]},{"Number":"142","Name":"Aerodactyl","Classification":"Fossil Pokemon","Type I":["Rock"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Ice","Rock","Steel"],"Fast Attack(s)":["Bite","Steel Wing"],"Weight":"59.0 kg","Height":"1.8 m"},{"Number":"143","Name":"Snorlax","Classification":"Sleeping Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"460.0 kg","Height":"2.1 m"},{"Number":"144","Name":"Articuno","Classification":"Freeze Pokemon","Type I":["Ice"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Rock","Steel"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"55.4 kg","Height":"1.7 m"},{"Number":"145","Name":"Zapdos","Classification":"Electric Pokemon","Type I":["Electric"],"Type II":["Flying"],"Weaknesses":["Ice","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"52.6 kg","Height":"1.6 m"},{"Number":"146","Name":"Moltres","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"60.0 kg","Height":"2.0 m"},{"Number":"147","Name":"Dratini","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"3.3 kg","Height":"1.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Dratini candies"}},{"Number":"148","Name":"Dragonair","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"16.5 kg","Height":"4.0 m","Next Evolution Requirements":{"Amount":100,"Name":"Dratini candies"},"Next evolution(s)":[{"Number":"149","Name":"Dragonite"}]},{"Number":"149","Name":"Dragonite","Classification":"Dragon Pokemon","Type I":["Dragon"],"Type II":["Flying"],"Weaknesses":["Ice","Rock","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath","Steel Wing"],"Weight":"210.0 kg","Height":"2.2 m","Previous evolution(s)":[{"Number":"148","Name":"Dragonair"}]},{"Number":"150","Name":"Mewtwo","Classification":"Genetic Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"122.0 kg","Height":"2.0 m"},{"Number":"151","Name":"Mew","Classification":"New Species Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.4 m"}] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..95a32f8ceb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: '2' +services: + bot1-pokego: + build: . + volumes: + - ./configs/config.json:/usr/src/app/configs/config.json + stdin_open: true + tty: true + bot1-pokegoweb: + image: python:2.7 + ports: + - "8000:8000" + volumes_from: + - bot1-pokego + volumes: + - ./configs/userdata.js:/usr/src/app/web/config/userdata.js + working_dir: /usr/src/app/web + command: bash -c "echo 'Serving HTTP on 0.0.0.0 port 8000' && python -m SimpleHTTPServer > /dev/null 2>&1" + depends_on: + - bot1-pokego \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000000..c11992bb0c --- /dev/null +++ b/install.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Setup Python virtualenv +echo "Setting up Python virtualenv..." +eval "virtualenv ." +eval "source bin/activate" +echo "Python virtualenv setup successfully." + +# Install pip requirements +echo "Installing pip requirements..." +eval "pip install -r requirements.txt" +echo "Installed pip requirements." +echo "Installing and updating git submodules..." + +# Install git submodules +eval "cd ./web && git submodule init && cd .." +eval "git submodule update" +echo "Done." +echo "Please create and setup configs/config.json. Then, run 'python pokecli.py --config ./configs/config.json' or './run.sh' on Mac/Linux" \ No newline at end of file diff --git a/pokecli.py b/pokecli.py old mode 100755 new mode 100644 index 963b96bf20..3d1b3736e7 --- a/pokecli.py +++ b/pokecli.py @@ -25,29 +25,134 @@ Author: tjado """ -import os -import re -import json import argparse -import time +import codecs +import json +import logging +import os import ssl import sys -import codecs +import time +from datetime import timedelta from getpass import getpass -import logging -import requests -from pokemongo_bot import logger -from pokemongo_bot import PokemonGoBot -from pokemongo_bot.cell_workers.utils import print_green, print_yellow, print_red +from pgoapi.exceptions import NotLoggedInException, ServerSideRequestThrottlingException, ServerBusyOrOfflineException +from geopy.exc import GeocoderQuotaExceeded + +from pokemongo_bot import PokemonGoBot, TreeConfigBuilder +from pokemongo_bot.health_record import BotEvent if sys.version_info >= (2, 7, 9): ssl._create_default_https_context = ssl._create_unverified_context +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s [%(name)10s] [%(levelname)s] %(message)s') +logger = logging.getLogger('cli') +logger.setLevel(logging.INFO) + +def main(): + try: + logger.info('PokemonGO Bot v1.0') + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + sys.stderr = codecs.getwriter('utf8')(sys.stderr) + + config = init_config() + if not config: + return + + logger.info('Configuration initialized') + health_record = BotEvent(config) + health_record.login_success() + + finished = False + + while not finished: + try: + bot = PokemonGoBot(config) + bot.start() + tree = TreeConfigBuilder(bot, config.raw_tasks).build() + bot.workers = tree + bot.metrics.capture_stats() + + bot.event_manager.emit( + 'bot_start', + sender=bot, + level='info', + formatted='Starting bot...' + ) + + while True: + bot.tick() + + except KeyboardInterrupt: + bot.event_manager.emit( + 'bot_exit', + sender=bot, + level='info', + formatted='Exiting bot.' + ) + finished = True + report_summary(bot) + + except NotLoggedInException: + wait_time = config.reconnecting_timeout * 60 + bot.event_manager.emit( + 'api_error', + sender=bot, + level='info', + formmated='Log logged in, reconnecting in {:s}'.format(wait_time) + ) + time.sleep(wait_time) + except ServerBusyOrOfflineException: + bot.event_manager.emit( + 'api_error', + sender=bot, + level='info', + formatted='Server busy or offline' + ) + except ServerSideRequestThrottlingException: + bot.event_manager.emit( + 'api_error', + sender=bot, + level='info', + formatted='Server is throttling, reconnecting in 30 seconds' + ) + time.sleep(30) + + except GeocoderQuotaExceeded: + raise Exception("Google Maps API key over requests limit.") + except Exception as e: + # always report session summary and then raise exception + if bot: + report_summary(bot) + + raise e + +def report_summary(bot): + if bot.metrics.start_time is None: + return # Bot didn't actually start, no metrics to show. + + metrics = bot.metrics + metrics.capture_stats() + logger.info('') + logger.info('Ran for {}'.format(metrics.runtime())) + logger.info('Total XP Earned: {} Average: {:.2f}/h'.format(metrics.xp_earned(), metrics.xp_per_hour())) + logger.info('Travelled {:.2f}km'.format(metrics.distance_travelled())) + logger.info('Visited {} stops'.format(metrics.visits['latest'] - metrics.visits['start'])) + logger.info('Encountered {} pokemon, {} caught, {} released, {} evolved, {} never seen before' + .format(metrics.num_encounters(), metrics.num_captures(), metrics.releases, + metrics.num_evolutions(), metrics.num_new_mons())) + logger.info('Threw {} pokeball{}'.format(metrics.num_throws(), '' if metrics.num_throws() == 1 else 's')) + logger.info('Earned {} Stardust'.format(metrics.earned_dust())) + logger.info('') + if metrics.highest_cp is not None: + logger.info('Highest CP Pokemon: {}'.format(metrics.highest_cp['desc'])) + if metrics.most_perfect is not None: + logger.info('Most Perfect Pokemon: {}'.format(metrics.most_perfect['desc'])) def init_config(): parser = argparse.ArgumentParser() - config_file = "config.json" - release_config_json = "release_config.json" + config_file = "configs/config.json" web_dir = "web" # If config file exists, load variables from json @@ -55,165 +160,333 @@ def init_config(): # Select a config file code parser.add_argument("-cf", "--config", help="Config File to use") - config_arg = unicode(parser.parse_args().config) - if os.path.isfile(config_arg): + config_arg = parser.parse_known_args() and parser.parse_known_args()[0].config or None + if config_arg and os.path.isfile(config_arg): with open(config_arg) as data: load.update(json.load(data)) elif os.path.isfile(config_file): + logger.info('No config argument specified, checking for /configs/config.json') with open(config_file) as data: load.update(json.load(data)) + else: + logger.info('Error: No /configs/config.json or specified config') # Read passed in Arguments required = lambda x: not x in load - parser.add_argument("-a", - "--auth_service", - help="Auth Service ('ptc' or 'google')", - required=required("auth_service")) - parser.add_argument("-u", "--username", help="Username") - parser.add_argument("-p", "--password", help="Password") - parser.add_argument("-l", "--location", help="Location") - parser.add_argument("-lc", - "--location_cache", - help="Bot will start at last known location", - type=bool, - default=False) - parser.add_argument("-m", - "--mode", - help="Farming Mode", - type=str, - default="all") - parser.add_argument( - "-w", - "--walk", + add_config( + parser, + load, + short_flag="-a", + long_flag="--auth_service", + help="Auth Service ('ptc' or 'google')", + required=required("auth_service"), + default=None + ) + add_config( + parser, + load, + short_flag="-u", + long_flag="--username", + help="Username", + default=None + ) + add_config( + parser, + load, + short_flag="-ws", + long_flag="--websocket.server_url", + help="Connect to websocket server at given url", + default=False + ) + add_config( + parser, + load, + short_flag="-wss", + long_flag="--websocket.start_embedded_server", + help="Start embedded websocket server", + default=False + ) + add_config( + parser, + load, + short_flag="-wsr", + long_flag="--websocket.remote_control", + help="Enable remote control through websocket (requires websocekt server url)", + default=False + ) + add_config( + parser, + load, + short_flag="-p", + long_flag="--password", + help="Password", + default=None + ) + add_config( + parser, + load, + short_flag="-l", + long_flag="--location", + help="Location", + type=parse_unicode_str, + default='' + ) + add_config( + parser, + load, + short_flag="-lc", + long_flag="--location_cache", + help="Bot will start at last known location", + type=bool, + default=False + ) + add_config( + parser, + load, + long_flag="--forts.spin", + help="Enable Spinning Pokestops", + type=bool, + default=True, + ) + add_config( + parser, + load, + short_flag="-w", + long_flag="--walk", help= "Walk instead of teleport with given speed (meters per second, e.g. 2.5)", type=float, - default=2.5) - parser.add_argument("-k", - "--gmapkey", - help="Set Google Maps API KEY", - type=str, - default=None) - parser.add_argument( - "-ms", - "--max_steps", - help= - "Set the steps around your initial location(DEFAULT 5 mean 25 cells around your location)", - type=int, - default=50) - parser.add_argument( - "-it", - "--initial_transfer", - help= - "Transfer all duplicate pokemon with same ID on bot start, except pokemon with highest CP. Accepts a number to prevent transferring pokemon with a CP above the provided value. Default is 0 (aka transfer none).", - type=int, - default=0) - parser.add_argument("-d", - "--debug", - help="Debug Mode", - type=bool, - default=False) - parser.add_argument("-t", - "--test", - help="Only parse the specified location", - type=bool, - default=False) - parser.add_argument( - "-du", - "--distance_unit", - help= - "Set the unit to display distance in (e.g, km for kilometers, mi for miles, ft for feet)", + default=2.5 + ) + add_config( + parser, + load, + short_flag="-k", + long_flag="--gmapkey", + help="Set Google Maps API KEY", type=str, - default="km") - - parser.add_argument( - "-if", - "--item_filter", - help= - "Pass a list of unwanted items to recycle when collected at a Pokestop (e.g, \"101,102,103,104\" to recycle potions when collected)", + default=None + ) + add_config( + parser, + load, + short_flag="-e", + long_flag="--show_events", + help="Show events", + type=bool, + default=False + ) + add_config( + parser, + load, + short_flag="-d", + long_flag="--debug", + help="Debug Mode", + type=bool, + default=False + ) + add_config( + parser, + load, + short_flag="-t", + long_flag="--test", + help="Only parse the specified location", + type=bool, + default=False + ) + add_config( + parser, + load, + short_flag="-du", + long_flag="--distance_unit", + help="Set the unit to display distance in (e.g, km for kilometers, mi for miles, ft for feet)", type=str, - default=[]) - - parser.add_argument("-ev", - "--evolve_all", - help="(Batch mode) Pass \"all\" or a list of pokemons to evolve (e.g., \"Pidgey,Weedle,Caterpie\"). Bot will start by attempting to evolve all pokemons. Great after popping a lucky egg!", - type=str, - default=[]) - - parser.add_argument("-ec", - "--evolve_captured", - help="(Ad-hoc mode) Bot will attempt to evolve all the pokemons captured!", - type=bool, - default=False) + default='km' + ) + add_config( + parser, + load, + short_flag="-ec", + long_flag="--evolve_captured", + help="(Ad-hoc mode) Pass \"all\" or a list of pokemon to evolve (e.g., \"Pidgey,Weedle,Caterpie\"). Bot will attempt to evolve all the pokemon captured!", + type=str, + default=[] + ) + add_config( + parser, + load, + short_flag="-rt", + long_flag="--reconnecting_timeout", + help="Timeout between reconnecting if error occured (in minutes, e.g. 15)", + type=float, + default=15.0 + ) + add_config( + parser, + load, + short_flag="-hr", + long_flag="--health_record", + help="Send anonymous bot event to GA for bot health record. Set \"health_record\":false if you need disable it.", + type=bool, + default=True + ) + add_config( + parser, + load, + short_flag="-ac", + long_flag="--forts.avoid_circles", + help="Avoids circles (pokestops) of the max size set in max_circle_size flag", + type=bool, + default=False, + ) + add_config( + parser, + load, + short_flag="-mcs", + long_flag="--forts.max_circle_size", + help="If avoid_circles flag is set, this flag specifies the maximum size of circles (pokestops) avoided", + type=int, + default=10, + ) + 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)", + type=float, + default=1.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)", + type=float, + default=1.0 + ) + add_config( + parser, + load, + long_flag="--map_object_cache_time", + help="Amount of seconds to keep the map object in cache (bypass Niantic throttling)", + type=float, + default=5.0 + ) + # Start to parse other attrs config = parser.parse_args() if not config.username and 'username' not in load: config.username = raw_input("Username: ") if not config.password and 'password' not in load: config.password = getpass("Password: ") - # Passed in arguments should trump - for key in config.__dict__: - if key in load: - config.__dict__[key] = load[key] + config.catch = load.get('catch', {}) + config.release = load.get('release', {}) + config.action_wait_max = load.get('action_wait_max', 4) + config.action_wait_min = load.get('action_wait_min', 1) + config.raw_tasks = load.get('tasks', []) + + config.vips = load.get('vips', {}) + + if config.map_object_cache_time < 0.0: + parser.error("--map_object_cache_time is out of range! (should be >= 0.0)") + return None + + if len(config.raw_tasks) == 0: + logging.error("No tasks are configured. Did you mean to configure some behaviors? Read https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files#configuring-tasks for more information") + return None if config.auth_service not in ['ptc', 'google']: logging.error("Invalid Auth service specified! ('ptc' or 'google')") return None + def task_configuration_error(flag_name): + parser.error(""" + \"{}\" was removed from the configuration options. + You can now change the behavior of the bot by modifying the \"tasks\" key. + Read https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files#configuring-tasks for more information. + """.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', 'max_steps'] + for flag in old_flags: + if flag in load: + task_configuration_error(flag) + return None + + nested_old_flags = [('forts', 'spin'), ('forts', 'move_to_spin'), ('navigator', 'path_mode'), ('navigator', 'path_file'), ('navigator', 'type')] + for outer, inner in nested_old_flags: + if load.get(outer, {}).get(inner, None): + task_configuration_error('{}.{}'.format(outer, inner)) + return None + + if (config.evolve_captured + and (not isinstance(config.evolve_captured, str) + or str(config.evolve_captured).lower() in ["true", "false"])): + parser.error('"evolve_captured" should be list of pokemons: use "all" or "none" to match all ' + + 'or none of the pokemons, or use a comma separated list such as "Pidgey,Weedle,Caterpie"') + return None + if not (config.location or config.location_cache): parser.error("Needs either --use-location-cache or --location.") return None - if config.item_filter: - config.item_filter = [str(item_id) for item_id in config.item_filter.split(',')] + 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 - config.release_config = {} - if os.path.isfile(release_config_json): - with open(release_config_json) as data: - config.release_config.update(json.load(data)) + 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: + try: os.makedirs(web_dir) except OSError: if not os.path.isdir(web_dir): raise - if config.evolve_all: - config.evolve_all = [str(pokemon_name) for pokemon_name in config.evolve_all.split(',')] + if config.evolve_captured and isinstance(config.evolve_captured, str): + config.evolve_captured = [str(pokemon_name).strip() for pokemon_name in config.evolve_captured.split(',')] + fix_nested_config(config) return config +def add_config(parser, json_config, short_flag=None, long_flag=None, **kwargs): + if not long_flag: + raise Exception('add_config calls requires long_flag parameter!') -def main(): - # log settings - # log format - #logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(module)10s] [%(levelname)5s] %(message)s') + full_attribute_path = long_flag.split('--')[1] + attribute_name = full_attribute_path.split('.')[-1] - sys.stdout = codecs.getwriter('utf8')(sys.stdout) - sys.stderr = codecs.getwriter('utf8')(sys.stderr) + if '.' in full_attribute_path: # embedded config! + embedded_in = full_attribute_path.split('.')[0: -1] + for level in embedded_in: + json_config = json_config.get(level, {}) - config = init_config() - if not config: - return + if 'default' in kwargs: + kwargs['default'] = json_config.get(attribute_name, kwargs['default']) + if short_flag: + args = (short_flag, long_flag) + else: + args = (long_flag,) + parser.add_argument(*args, **kwargs) - logger.log('[x] PokemonGO Bot v1.0', 'green') - logger.log('[x] Configuration initialized', 'yellow') - try: - bot = PokemonGoBot(config) - bot.start() +def fix_nested_config(config): + config_dict = config.__dict__ - logger.log('[x] Starting PokemonGo Bot....', 'green') + for key, value in config_dict.iteritems(): + if '.' in key: + new_key = key.replace('.', '_') + config_dict[new_key] = value + del config_dict[key] - while True: - bot.take_step() - - except KeyboardInterrupt: - logger.log('[x] Exiting PokemonGo Bot', 'red') - # TODO Add number of pokemon catched, pokestops visited, highest CP - # pokemon catched, etc. +def parse_unicode_str(string): + try: + return string.decode('utf8') + except UnicodeEncodeError: + return string if __name__ == '__main__': diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 6dc13bbbda..045abd82e9 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -1,424 +1,922 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals -import logging -import googlemaps +import datetime import json +import logging +import os import random -import threading -import datetime -import sys -import yaml -import logger import re +import sys +import time + +from geopy.geocoders import GoogleV3 from pgoapi import PGoApi -from cell_workers import PokemonCatchWorker, SeenFortWorker, MoveToFortWorker, InitialTransferWorker, EvolveAllWorker +from pgoapi.utilities import f2i, get_cell_ids + +import cell_workers +from api_wrapper import ApiWrapper from cell_workers.utils import distance +from event_manager import EventManager from human_behaviour import sleep -from stepper import Stepper -from geopy.geocoders import GoogleV3 -from math import radians, sqrt, sin, cos, atan2 from item_list import Item +from metrics import Metrics +from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler +from pokemongo_bot.socketio_server.runner import SocketIoRunner +from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl +from worker_result import WorkerResult +from tree_config_builder import ConfigException, TreeConfigBuilder class PokemonGoBot(object): + @property + 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.pokemon_list = json.load(open('data/pokemon.json')) - self.item_list = json.load(open('data/items.json')) + self.fort_timeouts = dict() + self.pokemon_list = json.load( + open(os.path.join('data', 'pokemon.json')) + ) + self.item_list = json.load(open(os.path.join('data', 'items.json'))) + self.metrics = Metrics(self) + self.latest_inventory = None + self.cell = None + self.recent_forts = [None] * config.forts_max_circle_size + self.tick_count = 0 + self.softban = False + self.start_position = None + self.last_map_object = None + self.last_time_map_object = 0 + self.logger = logging.getLogger(type(self).__name__) + + # Make our own copy of the workers for this instance + self.workers = [] def start(self): + self._setup_event_system() self._setup_logging() self._setup_api() - self.stepper = Stepper(self) + random.seed() - def take_step(self): - self.stepper.take_step() - - def work_on_cell(self, cell, position, include_fort_on_path): - if self.config.evolve_all: - # Run evolve all once. Flip the bit. - print('[#] Attempting to evolve all pokemons ...') - worker = EvolveAllWorker(self) - worker.work() - self.config.evolve_all = [] - - self._filter_ignored_pokemons(cell) - - if (self.config.mode == "all" or self.config.mode == - "poke") and 'catchable_pokemons' in cell and len(cell[ - 'catchable_pokemons']) > 0: - logger.log('[#] Something rustles nearby!') - # Sort all by distance from current pos- eventually this should - # build graph & A* it - cell['catchable_pokemons'].sort( - key= - lambda x: distance(self.position[0], self.position[1], x['latitude'], x['longitude'])) - - user_web_catchable = 'web/catchable-%s.json' % (self.config.username) - for pokemon in cell['catchable_pokemons']: - with open(user_web_catchable, 'w') as outfile: - json.dump(pokemon, outfile) - - if self.catch_pokemon(pokemon) == PokemonCatchWorker.NO_POKEBALLS: - break - with open(user_web_catchable, 'w') as outfile: - json.dump({}, outfile) - - if (self.config.mode == "all" or self.config.mode == "poke" - ) and 'wild_pokemons' in cell and len(cell['wild_pokemons']) > 0: - # Sort all by distance from current pos- eventually this should - # build graph & A* it - cell['wild_pokemons'].sort( - key= - lambda x: distance(self.position[0], self.position[1], x['latitude'], x['longitude'])) - for pokemon in cell['wild_pokemons']: - if self.catch_pokemon(pokemon) == PokemonCatchWorker.NO_POKEBALLS: - break - if (self.config.mode == "all" or - self.config.mode == "farm") and include_fort_on_path: - if 'forts' in cell: - # Only include those with a lat/long - forts = [fort - for fort in cell['forts'] - if 'latitude' in fort and 'type' in fort] - gyms = [gym for gym in cell['forts'] if 'gym_points' in gym] - - # Sort all by distance from current pos- eventually this should - # build graph & A* it - forts.sort(key=lambda x: distance(self.position[ - 0], self.position[1], x['latitude'], x['longitude'])) - for fort in forts: - worker = MoveToFortWorker(fort, self) - worker.work() - - worker = SeenFortWorker(fort, self) - hack_chain = worker.work() - if hack_chain > 10: - #print('need a rest') - break + def _setup_event_system(self): + handlers = [LoggingHandler()] + if self.config.websocket_server_url: + if self.config.websocket_start_embedded_server: + self.sio_runner = SocketIoRunner(self.config.websocket_server_url) + self.sio_runner.start_listening_async() + + websocket_handler = SocketIoHandler( + self, + self.config.websocket_server_url + ) + handlers.append(websocket_handler) + + if self.config.websocket_remote_control: + remote_control = WebsocketRemoteControl(self).start() + + + self.event_manager = EventManager(*handlers) + self._register_events() + if self.config.show_events: + self.event_manager.event_report() + sys.exit(1) + + # Registering event: + # self.event_manager.register_event("location", parameters=['lat', 'lng']) + # + # Emitting event should be enough to add logging and send websocket + # message: : + # self.event_manager.emit('location', 'level'='info', data={'lat': 1, 'lng':1}), + + def _register_events(self): + self.event_manager.register_event( + 'location_found', + parameters=('position', 'location') + ) + self.event_manager.register_event('api_error') + self.event_manager.register_event('config_error') + + self.event_manager.register_event('login_started') + self.event_manager.register_event('login_failed') + self.event_manager.register_event('login_successful') + + self.event_manager.register_event('set_start_location') + self.event_manager.register_event('load_cached_location') + self.event_manager.register_event('location_cache_ignored') + self.event_manager.register_event( + 'position_update', + parameters=( + 'current_position', + 'last_position', + 'distance', # optional + 'distance_unit' # optional + ) + ) + self.event_manager.register_event('location_cache_error') + + self.event_manager.register_event('bot_start') + self.event_manager.register_event('bot_exit') + + # sleep stuff + self.event_manager.register_event( + 'next_sleep', + parameters=('time',) + ) + self.event_manager.register_event( + 'bot_sleep', + parameters=('time_in_seconds',) + ) + + # fort stuff + self.event_manager.register_event( + 'spun_fort', + parameters=( + 'fort_id', + 'latitude', + 'longitude' + ) + ) + self.event_manager.register_event( + 'lured_pokemon_found', + parameters=( + 'fort_id', + 'fort_name', + 'encounter_id', + 'latitude', + 'longitude' + ) + ) + self.event_manager.register_event( + 'moving_to_fort', + parameters=( + 'fort_name', + 'distance' + ) + ) + self.event_manager.register_event( + 'moving_to_lured_fort', + parameters=( + 'fort_name', + 'distance', + 'lure_distance' + ) + ) + self.event_manager.register_event( + 'spun_pokestop', + parameters=( + 'pokestop', 'exp', 'items' + ) + ) + self.event_manager.register_event( + 'pokestop_empty', + parameters=('pokestop',) + ) + self.event_manager.register_event( + 'pokestop_out_of_range', + parameters=('pokestop',) + ) + self.event_manager.register_event( + 'pokestop_on_cooldown', + parameters=('pokestop', 'minutes_left') + ) + self.event_manager.register_event( + 'unknown_spin_result', + parameters=('status_code',) + ) + self.event_manager.register_event('pokestop_searching_too_often') + self.event_manager.register_event('arrived_at_fort') + + # pokemon stuff + self.event_manager.register_event( + 'catchable_pokemon', + parameters=( + 'pokemon_id', + 'spawn_point_id', + 'encounter_id', + 'latitude', + 'longitude', + 'expiration_timestamp_ms' + ) + ) + self.event_manager.register_event( + 'pokemon_appeared', + parameters=( + 'pokemon', + 'cp', + 'iv', + 'iv_display', + ) + ) + self.event_manager.register_event( + 'pokemon_catch_rate', + parameters=( + 'catch_rate', + 'berry_name', + 'berry_count' + ) + ) + self.event_manager.register_event( + 'threw_berry', + parameters=( + 'berry_name', + 'new_catch_rate' + ) + ) + self.event_manager.register_event( + 'threw_pokeball', + parameters=( + 'pokeball', + 'success_percentage', + 'count_left' + ) + ) + self.event_manager.register_event( + 'pokemon_fled', + parameters=('pokemon',) + ) + self.event_manager.register_event( + 'pokemon_vanished', + parameters=('pokemon',) + ) + self.event_manager.register_event( + 'pokemon_caught', + parameters=( + 'pokemon', + 'cp', 'iv', 'iv_display', 'exp' + ) + ) + self.event_manager.register_event( + 'pokemon_evolved', + parameters=('pokemon', 'iv', 'cp') + ) + self.event_manager.register_event( + 'pokemon_evolve_fail', + parameters=('pokemon',) + ) + 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') + + + # level up stuff + self.event_manager.register_event( + 'level_up', + parameters=( + 'previous_level', + 'current_level' + ) + ) + self.event_manager.register_event( + 'level_up_reward', + parameters=('items',) + ) + + # lucky egg + self.event_manager.register_event( + 'used_lucky_egg', + parameters=('amount_left',) + ) + self.event_manager.register_event('lucky_egg_error') + + # softban + self.event_manager.register_event('softban') + self.event_manager.register_event('softban_fix') + self.event_manager.register_event('softban_fix_done') + + # egg incubating + self.event_manager.register_event( + 'incubate_try', + parameters=( + 'incubator_id', + 'egg_id' + ) + ) + self.event_manager.register_event( + 'incubate', + parameters=('distance_in_km',) + ) + self.event_manager.register_event( + 'next_egg_incubates', + parameters=('distance_in_km',) + ) + self.event_manager.register_event('incubator_already_used') + self.event_manager.register_event('egg_already_incubating') + self.event_manager.register_event( + 'egg_hatched', + parameters=( + 'pokemon', + 'cp', 'iv', 'exp', 'stardust', 'candy' + ) + ) + + # discard item + self.event_manager.register_event( + 'item_discarded', + parameters=( + 'amount', 'item', 'maximum' + ) + ) + self.event_manager.register_event( + 'item_discard_fail', + parameters=('item',) + ) + + # inventory + self.event_manager.register_event('inventory_full') + + # release + self.event_manager.register_event( + 'keep_best_release', + parameters=( + 'amount', 'pokemon', 'criteria' + ) + ) + self.event_manager.register_event( + 'future_pokemon_release', + parameters=( + 'pokemon', 'cp', 'iv', 'below_iv', 'below_cp', 'cp_iv_logic' + ) + ) + self.event_manager.register_event( + 'pokemon_release', + parameters=('pokemon', 'cp', 'iv') + ) + + # polyline walker + self.event_manager.register_event( + 'polyline_request', + parameters=('url',) + ) + + # cluster + self.event_manager.register_event( + 'found_cluster', + parameters=( + 'num_points', 'forts', 'radius', 'distance' + ) + ) + self.event_manager.register_event( + 'arrived_at_cluster', + parameters=( + 'forts', 'radius' + ) + ) + + # rename + self.event_manager.register_event( + 'rename_pokemon', + parameters=( + 'old_name', 'current_name' + ) + ) + self.event_manager.register_event( + 'pokemon_nickname_invalid', + parameters=('nickname',) + ) + self.event_manager.register_event('unset_pokemon_nickname') + + def tick(self): + self.cell = self.get_meta_cell() + self.tick_count += 1 + + # Check if session token has expired + self.check_session(self.position[0:2]) + + for worker in self.workers: + if worker.work() == WorkerResult.RUNNING: + return + + def get_meta_cell(self): + location = self.position[0:2] + cells = self.find_close_cells(*location) + + # Combine all cells into a single dict of the items we care about. + forts = [] + wild_pokemons = [] + catchable_pokemons = [] + for cell in cells: + if "forts" in cell and len(cell["forts"]): + forts += cell["forts"] + if "wild_pokemons" in cell and len(cell["wild_pokemons"]): + 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 { + "forts": forts, + "wild_pokemons": wild_pokemons, + "catchable_pokemons": catchable_pokemons + } + # If there are no forts present in the data from the server, keep our existing fort data and only update the pokemon cells. + else: + return { + "forts": self.cell["forts"], + "wild_pokemons": wild_pokemons, + "catchable_pokemons": catchable_pokemons + } + + def update_web_location(self, cells=[], lat=None, lng=None, alt=None): + # we can call the function with no arguments and still get the position + # and map_cells + if lat is None: + lat = self.api._position_lat + if lng is None: + lng = self.api._position_lng + if alt is None: + alt = 0 + + if cells == []: + location = self.position[0:2] + cells = self.find_close_cells(*location) + + # insert detail info about gym to fort + for cell in cells: + if 'forts' in cell: + for fort in cell['forts']: + if fort.get('type') != 1: + 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') + ) + fort['gym_details'] = response_gym_details.get( + 'responses', {} + ).get('GET_GYM_DETAILS', None) + + user_data_cells = "data/cells-%s.json" % self.config.username + with open(user_data_cells, 'w') as outfile: + json.dump(cells, outfile) + + user_web_location = os.path.join( + 'web', 'location-%s.json' % self.config.username + ) + # alt is unused atm but makes using *location easier + try: + with open(user_web_location, 'w') as outfile: + json.dump({ + 'lat': lat, + 'lng': lng, + 'alt': alt, + 'cells': cells + }, outfile) + except IOError as e: + self.logger.info('[x] Error while opening location file: %s' % e) + + user_data_lastlocation = os.path.join( + 'data', 'last-location-%s.json' % self.config.username + ) + try: + with open(user_data_lastlocation, 'w') as outfile: + json.dump({'lat': lat, 'lng': lng, 'start_position': self.start_position}, outfile) + except IOError as e: + self.logger.info('[x] Error while opening location file: %s' % e) + + def find_close_cells(self, lat, lng): + cellid = get_cell_ids(lat, lng) + timestamp = [0, ] * len(cellid) + response_dict = self.get_map_objects(lat, lng, timestamp, cellid) + map_objects = response_dict.get( + 'responses', {} + ).get('GET_MAP_OBJECTS', {}) + status = map_objects.get('status', None) + + map_cells = [] + if status and status == 1: + map_cells = map_objects['map_cells'] + position = (lat, lng, 0) + map_cells.sort( + key=lambda x: distance( + lat, + lng, + x['forts'][0]['latitude'], + x['forts'][0]['longitude']) if x.get('forts', []) else 1e6 + ) + return map_cells def _setup_logging(self): - self.log = logging.getLogger(__name__) # log settings # log format - logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s [%(module)10s] [%(levelname)5s] %(message)s') if self.config.debug: + log_level = logging.DEBUG logging.getLogger("requests").setLevel(logging.DEBUG) + logging.getLogger("websocket").setLevel(logging.DEBUG) + logging.getLogger("socketio").setLevel(logging.DEBUG) + logging.getLogger("engineio").setLevel(logging.DEBUG) + logging.getLogger("socketIO-client").setLevel(logging.DEBUG) logging.getLogger("pgoapi").setLevel(logging.DEBUG) logging.getLogger("rpc_api").setLevel(logging.DEBUG) else: + log_level = logging.ERROR logging.getLogger("requests").setLevel(logging.ERROR) + logging.getLogger("websocket").setLevel(logging.ERROR) + logging.getLogger("socketio").setLevel(logging.ERROR) + logging.getLogger("engineio").setLevel(logging.ERROR) + logging.getLogger("socketIO-client").setLevel(logging.ERROR) logging.getLogger("pgoapi").setLevel(logging.ERROR) logging.getLogger("rpc_api").setLevel(logging.ERROR) - def _setup_api(self): - # instantiate pgoapi - self.api = PGoApi() + logging.basicConfig( + level=log_level, + format='%(asctime)s [%(name)10s] [%(levelname)s] %(message)s' + ) + def check_session(self, position): + # Check session expiry + if self.api._auth_provider and self.api._auth_provider._ticket_expire: + + # 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') + return + + remaining_time = \ + self.api._auth_provider._ticket_expire / 1000 - time.time() - # check if the release_config file exists + if remaining_time < 60: + self.logger.info("Session stale, re-logging in", 'yellow') + position = self.position + self.api = ApiWrapper() + self.position = position + self.login() + + @staticmethod + def is_numeric(s): try: - with open('release_config.json') as file: - pass - except: - # the file does not exist, warn the user and exit. - logger.log('[#] IMPORTANT: Rename and configure release_config.json.example for your Pokemon release logic first!', 'red') - exit(0) + float(s) + return True + except ValueError: + return False + + def login(self): + self.event_manager.emit( + 'login_started', + sender=self, + level='info', + formatted="Login procedure started." + ) + 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)): + + self.event_manager.emit( + 'login_failed', + sender=self, + level='info', + formatted="Login error, server busy. Waiting 10 seconds to try again." + ) + time.sleep(10) + + self.event_manager.emit( + 'login_successful', + sender=self, + level='info', + formatted="Login successful." + ) + + def _setup_api(self): + # instantiate pgoapi + self.api = ApiWrapper() # provide player position on the earth self._set_starting_position() - if not self.api.login(self.config.auth_service, - str(self.config.username), - str(self.config.password)): - logger.log('Login Error, server busy', 'red') - exit(0) - + self.login() # chain subrequests (methods) into one RPC call + self._print_character_info() + + self.logger.info('') + self.update_inventory() + # send empty map_cells and then our position + self.update_web_location() + + def _print_character_info(self): # get player profile call # ---------------------- - self.api.get_player() - - response_dict = self.api.call() - #print('Response dictionary: \n\r{}'.format(json.dumps(response_dict, indent=2))) + response_dict = self.api.get_player() + # print('Response dictionary: \n\r{}'.format(json.dumps(response_dict, indent=2))) currency_1 = "0" currency_2 = "0" - player = response_dict['responses']['GET_PLAYER']['player_data'] + if response_dict: + self._player = response_dict['responses']['GET_PLAYER']['player_data'] + player = self._player + else: + self.logger.info( + "The API didn't return player info, servers are unstable - " + "retrying.", 'red' + ) + sleep(5) + self._print_character_info() # @@@ TODO: Convert this to d/m/Y H:M:S creation_date = datetime.datetime.fromtimestamp( player['creation_timestamp_ms'] / 1e3) + creation_date = creation_date.strftime("%Y/%m/%d %H:%M:%S") pokecoins = '0' stardust = '0' - balls_stock = self.pokeball_inventory() + items_stock = self.current_inventory() if 'amount' in player['currencies'][0]: pokecoins = player['currencies'][0]['amount'] if 'amount' in player['currencies'][1]: stardust = player['currencies'][1]['amount'] - - logger.log('[#] Username: {username}'.format(**player)) - logger.log('[#] Acccount Creation: {}'.format(creation_date)) - logger.log('[#] Bag Storage: {}/{}'.format( - self.get_inventory_count('item'), player['max_item_storage'])) - logger.log('[#] Pokemon Storage: {}/{}'.format( - self.get_inventory_count('pokemon'), player[ - 'max_pokemon_storage'])) - logger.log('[#] Stardust: {}'.format(stardust)) - logger.log('[#] Pokecoins: {}'.format(pokecoins)) - logger.log('[#] PokeBalls: ' + str(balls_stock[1])) - logger.log('[#] GreatBalls: ' + str(balls_stock[2])) - logger.log('[#] UltraBalls: ' + str(balls_stock[3])) - + self.logger.info('') + self.logger.info('--- {username} ---'.format(**player)) self.get_player_info() - - if self.config.initial_transfer: - worker = InitialTransferWorker(self) - worker.work() - - logger.log('[#]') - self.update_inventory() - - def catch_pokemon(self, pokemon): - worker = PokemonCatchWorker(pokemon, self) - return_value = worker.work() - - if return_value == PokemonCatchWorker.BAG_FULL: - worker = InitialTransferWorker(self) - worker.work() - - return return_value - - def drop_item(self, item_id, count): - self.api.recycle_inventory_item(item_id=item_id, count=count) - inventory_req = self.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 + self.logger.info( + 'Pokemon Bag: {}/{}'.format( + self.get_inventory_count('pokemon'), + player['max_pokemon_storage'] + ) + ) + self.logger.info( + 'Items: {}/{}'.format( + self.get_inventory_count('item'), + player['max_item_storage'] + ) + ) + self.logger.info( + 'Stardust: {}'.format(stardust) + + ' | Pokecoins: {}'.format(pokecoins) + ) + # Items Output + self.logger.info( + 'PokeBalls: ' + str(items_stock[1]) + + ' | GreatBalls: ' + str(items_stock[2]) + + ' | UltraBalls: ' + str(items_stock[3])) + + self.logger.info( + 'RazzBerries: ' + str(items_stock[701]) + + ' | BlukBerries: ' + str(items_stock[702]) + + ' | NanabBerries: ' + str(items_stock[703])) + + self.logger.info( + 'LuckyEgg: ' + str(items_stock[301]) + + ' | Incubator: ' + str(items_stock[902]) + + ' | TroyDisk: ' + str(items_stock[501])) + + self.logger.info( + 'Potion: ' + str(items_stock[101]) + + ' | SuperPotion: ' + str(items_stock[102]) + + ' | HyperPotion: ' + str(items_stock[103])) + + self.logger.info( + 'Incense: ' + str(items_stock[401]) + + ' | IncenseSpicy: ' + str(items_stock[402]) + + ' | IncenseCool: ' + str(items_stock[403])) + + self.logger.info( + 'Revive: ' + str(items_stock[201]) + + ' | MaxRevive: ' + str(items_stock[202])) + + self.logger.info('') + + def use_lucky_egg(self): + return self.api.use_item_xp_boost(item_id=301) + + def get_inventory(self): + if self.latest_inventory is None: + self.latest_inventory = self.api.get_inventory() + return self.latest_inventory def update_inventory(self): - self.api.get_inventory() - response = self.api.call() + response = self.get_inventory() self.inventory = list() - if 'responses' in response: - if 'GET_INVENTORY' in response['responses']: - if 'inventory_delta' in response['responses']['GET_INVENTORY']: - if 'inventory_items' in response['responses'][ - 'GET_INVENTORY']['inventory_delta']: - for item in response['responses']['GET_INVENTORY'][ - 'inventory_delta']['inventory_items']: - if not 'inventory_item_data' in item: - continue - if not 'item' in item['inventory_item_data']: - continue - if not 'item_id' in item['inventory_item_data'][ - 'item']: - continue - if not 'count' in item['inventory_item_data'][ - 'item']: - continue - self.inventory.append(item['inventory_item_data'][ - 'item']) - - def pokeball_inventory(self): - self.api.get_player().get_inventory() - - inventory_req = self.api.call() + inventory_items = response.get('responses', {}).get('GET_INVENTORY', {}).get( + 'inventory_delta', {}).get('inventory_items', {}) + if inventory_items: + for item in inventory_items: + item_info = item.get('inventory_item_data', {}).get('item', {}) + if {"item_id", "count"}.issubset(set(item_info.keys())): + self.inventory.append(item['inventory_item_data']['item']) + + def current_inventory(self): + inventory_req = self.get_inventory() inventory_dict = inventory_req['responses']['GET_INVENTORY'][ 'inventory_delta']['inventory_items'] - user_web_inventory = 'web/inventory-%s.json' % (self.config.username) + user_web_inventory = 'web/inventory-%s.json' % self.config.username + with open(user_web_inventory, 'w') as outfile: json.dump(inventory_dict, outfile) - # get player balls stock + # get player items stock # ---------------------- - balls_stock = {1: 0, 2: 0, 3: 0, 4: 0} + items_stock = {x.value: 0 for x in list(Item)} for item in inventory_dict: - try: - # print(item['inventory_item_data']['item']) - item_id = item['inventory_item_data']['item']['item_id'] - item_count = item['inventory_item_data']['item']['count'] - - if item_id == Item.ITEM_POKE_BALL.value: - # print('Poke Ball count: ' + str(item_count)) - balls_stock[1] = item_count - if item_id == Item.ITEM_GREAT_BALL.value: - # print('Great Ball count: ' + str(item_count)) - balls_stock[2] = item_count - if item_id == Item.ITEM_ULTRA_BALL.value: - # print('Ultra Ball count: ' + str(item_count)) - balls_stock[3] = item_count - except: - continue - return balls_stock + item_dict = item.get('inventory_item_data', {}).get('item', {}) + item_count = item_dict.get('count') + item_id = item_dict.get('item_id') - def item_inventory_count(self, id): - self.api.get_player().get_inventory() + if item_count and item_id: + if item_id in items_stock: + items_stock[item_id] = item_count + return items_stock - inventory_req = self.api.call() + def item_inventory_count(self, id): + inventory_req = self.get_inventory() inventory_dict = inventory_req['responses'][ 'GET_INVENTORY']['inventory_delta']['inventory_items'] + if id == 'all': + return self._all_items_inventory_count(inventory_dict) + else: + return self._item_inventory_count_per_id(id, inventory_dict) + + def _item_inventory_count_per_id(self, id, inventory_dict): item_count = 0 for item in inventory_dict: - try: - if item['inventory_item_data']['item']['item_id'] == int(id): - item_count = item[ - 'inventory_item_data']['item']['count'] - except: - continue - return item_count + item_dict = item.get('inventory_item_data', {}).get('item', {}) + item_id = item_dict.get('item_id', False) + item_count = item_dict.get('count', False) + if item_id == int(id) and item_count: + return item_count + return 0 + + def _all_items_inventory_count(self, inventory_dict): + item_count_dict = {} + + for item in inventory_dict: + item_dict = item.get('inventory_item_data', {}).get('item', {}) + item_id = item_dict.get('item_id', False) + item_count = item_dict.get('count', False) + if item_id and item_count: + item_count_dict[item_id] = item_count + + return item_count_dict def _set_starting_position(self): + self.event_manager.emit( + 'set_start_location', + sender=self, + level='info', + formatted='Setting start location.' + ) + + has_position = False + if self.config.test: # TODO: Add unit tests return if self.config.location: + location_str = self.config.location + location = self.get_pos_by_name(location_str.replace(" ", "")) + msg = "Location found: {location} {position}" + self.event_manager.emit( + 'location_found', + sender=self, + level='info', + formatted=msg, + data={ + 'location': location_str, + 'position': location + } + ) + + self.api.set_position(*location) + + self.event_manager.emit( + 'position_update', + sender=self, + level='info', + formatted="Now at {current_position}", + data={ + 'current_position': self.position, + 'last_position': '', + 'distance': '', + 'distance_unit': '' + } + ) + + self.start_position = self.position + + has_position = True + + if self.config.location_cache: try: - location_str = str(self.config.location) - location = (self._get_pos_by_name(location_str.replace(" ", ""))) - self.position = location - self.api.set_position(*self.position) - logger.log('') - logger.log(u'[x] Address found: {}'.format(self.config.location.decode( - 'utf-8'))) - logger.log('[x] Position in-game set as: {}'.format(self.position)) - logger.log('') - return - except: - logger.log('[x] The location given using -l could not be parsed. Checking for a cached location.') - pass - - if self.config.location_cache and not self.config.location: - try: - # # save location flag used to pull the last known location from # the location.json + self.event_manager.emit( + 'load_cached_location', + sender=self, + level='debug', + formatted='Loading cached location...' + ) with open('data/last-location-%s.json' % - (self.config.username)) as f: + self.config.username) as f: location_json = json.load(f) - - self.position = (location_json['lat'], - location_json['lng'], 0.0) - self.api.set_position(*self.position) - - logger.log('') - logger.log( - '[x] Last location flag used. Overriding passed in location') - logger.log( - '[x] Last in-game location was set as: {}'.format( - self.position)) - logger.log('') - - return - except: - if not self.config.location: + location = ( + location_json['lat'], + location_json['lng'], + 0.0 + ) + + # If location has been set in config, only use cache if starting position has not differed + if has_position and 'start_position' in location_json: + last_start_position = tuple(location_json.get('start_position', [])) + + # Start position has to have been set on a previous run to do this check + if last_start_position and last_start_position != self.start_position: + msg = 'Going to a new place, ignoring cached location.' + self.event_manager.emit( + 'location_cache_ignored', + sender=self, + level='debug', + formatted=msg + ) + return + + self.api.set_position(*location) + self.event_manager.emit( + 'position_update', + sender=self, + level='debug', + formatted='Loaded location {current_position} from cache', + data={ + 'current_position': location, + 'last_position': '', + 'distance': '', + 'distance_unit': '' + } + ) + + has_position = True + except Exception: + if has_position is False: sys.exit( - "No cached Location. Please specify initial location.") - else: - pass - - def _get_pos_by_name(self, location_name): + "No cached Location. Please specify initial location." + ) + self.event_manager.emit( + 'location_cache_error', + sender=self, + level='debug', + formatted='Parsing cached location failed.' + ) + + def get_pos_by_name(self, location_name): # Check if the given location is already a coordinate. if ',' in location_name: - possibleCoordinates = re.findall("[-]?\d{1,3}[.]\d{6,7}", location_name) - if len(possibleCoordinates) == 2: - # 2 matches, this must be a coordinate. We'll bypass the Google geocode so we keep the exact location. - logger.log( - '[x] Coordinates found in passed in location, not geocoding.') - return (float(possibleCoordinates[0]), float(possibleCoordinates[1]), float("0.0")) + possible_coordinates = re.findall( + "[-]?\d{1,3}[.]\d{3,7}", location_name + ) + if len(possible_coordinates) == 2: + # 2 matches, this must be a coordinate. We'll bypass the Google + # geocode so we keep the exact location. + self.logger.info( + '[x] Coordinates found in passed in location, ' + 'not geocoding.' + ) + return float(possible_coordinates[0]), float(possible_coordinates[1]), float("0.0") geolocator = GoogleV3(api_key=self.config.gmapkey) loc = geolocator.geocode(location_name, timeout=10) - #self.log.info('Your given location: %s', loc.address.encode('utf-8')) - #self.log.info('lat/long/alt: %s %s %s', loc.latitude, loc.longitude, loc.altitude) - - return (loc.latitude, loc.longitude, loc.altitude) - - def _filter_ignored_pokemons(self, cell): - process_ignore = False - try: - with open("./data/catch-ignore.yml", 'r') as y: - ignores = yaml.load(y)['ignore'] - if len(ignores) > 0: - process_ignore = True - except Exception, e: - pass - - if process_ignore: - # - # remove any wild pokemon - try: - for p in cell['wild_pokemons'][:]: - pokemon_id = p['pokemon_data']['pokemon_id'] - pokemon_name = filter( - lambda x: int(x.get('Number')) == pokemon_id, - self.pokemon_list)[0]['Name'] - - if pokemon_name in ignores: - cell['wild_pokemons'].remove(p) - except KeyError: - pass - - # - # remove catchable pokemon - try: - for p in cell['catchable_pokemons'][:]: - pokemon_id = p['pokemon_id'] - pokemon_name = filter( - lambda x: int(x.get('Number')) == pokemon_id, - self.pokemon_list)[0]['Name'] - - if pokemon_name in ignores: - cell['catchable_pokemons'].remove(p) - except KeyError: - pass + return float(loc.latitude), float(loc.longitude), float(loc.altitude) def heartbeat(self): - self.api.get_player() - self.api.get_hatched_eggs() - self.api.get_inventory() - self.api.check_awarded_badges() - self.api.call() + # Remove forts that we can now spin again. + self.fort_timeouts = {id: timeout for id, timeout + in self.fort_timeouts.iteritems() + if timeout >= time.time() * 1000} + 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): - self.api.get_inventory() - response_dict = self.api.call() - if 'responses' in response_dict: - if 'GET_INVENTORY' in response_dict['responses']: - if 'inventory_delta' in response_dict['responses'][ - 'GET_INVENTORY']: - if 'inventory_items' in response_dict['responses'][ - 'GET_INVENTORY']['inventory_delta']: - pokecount = 0 - itemcount = 1 - for item in response_dict['responses'][ - 'GET_INVENTORY']['inventory_delta'][ - 'inventory_items']: - #print('item {}'.format(item)) - if 'inventory_item_data' in item: - if 'pokemon_data' in item[ - 'inventory_item_data']: - pokecount = pokecount + 1 - if 'item' in item['inventory_item_data']: - if 'count' in item['inventory_item_data'][ - 'item']: - itemcount = itemcount + \ - item['inventory_item_data'][ - 'item']['count'] + response_dict = self.get_inventory() + inventory_items = response_dict.get('responses', {}).get('GET_INVENTORY', {}).get( + 'inventory_delta', {}).get('inventory_items', {}) + if inventory_items: + pokecount = 0 + itemcount = 1 + for item in inventory_items: + if 'inventory_item_data' in item: + if 'pokemon_data' in item['inventory_item_data']: + pokecount += 1 + itemcount += item['inventory_item_data'].get('item', {}).get('count', 0) if 'pokemon' in what: return pokecount if 'item' in what: @@ -426,49 +924,70 @@ def get_inventory_count(self, what): return '0' def get_player_info(self): - self.api.get_inventory() - response_dict = self.api.call() - if 'responses' in response_dict: - if 'GET_INVENTORY' in response_dict['responses']: - if 'inventory_delta' in response_dict['responses'][ - 'GET_INVENTORY']: - if 'inventory_items' in response_dict['responses'][ - 'GET_INVENTORY']['inventory_delta']: - pokecount = 0 - itemcount = 1 - for item in response_dict['responses'][ - 'GET_INVENTORY']['inventory_delta'][ - 'inventory_items']: - #print('item {}'.format(item)) - if 'inventory_item_data' in item: - if 'player_stats' in item[ - 'inventory_item_data']: - playerdata = item['inventory_item_data'][ - 'player_stats'] - - nextlvlxp = ( - int(playerdata.get('next_level_xp', 0)) - - int(playerdata.get('experience', 0))) - - if 'level' in playerdata: - logger.log( - '[#] -- Level: {level}'.format( - **playerdata)) - - if 'experience' in playerdata: - logger.log( - '[#] -- Experience: {experience}'.format( - **playerdata)) - logger.log( - '[#] -- Experience until next level: {}'.format( - nextlvlxp)) - - if 'pokemons_captured' in playerdata: - logger.log( - '[#] -- Pokemon Captured: {pokemons_captured}'.format( - **playerdata)) - - if 'poke_stop_visits' in playerdata: - logger.log( - '[#] -- Pokestops Visited: {poke_stop_visits}'.format( - **playerdata)) + response_dict = self.get_inventory() + inventory_items = response_dict.get('responses', {}).get('GET_INVENTORY', {}).get( + 'inventory_delta', {}).get('inventory_items', {}) + if inventory_items: + pokecount = 0 + itemcount = 1 + for item in inventory_items: + # print('item {}'.format(item)) + playerdata = item.get('inventory_item_data', {}).get('player_stats') + if playerdata: + nextlvlxp = (int(playerdata.get('next_level_xp', 0)) - int(playerdata.get('experience', 0))) + + if 'level' in playerdata and 'experience' in playerdata: + self.logger.info( + 'Level: {level}'.format( + **playerdata) + + ' (Next Level: {} XP)'.format( + nextlvlxp) + + ' (Total: {experience} XP)' + ''.format(**playerdata)) + + if 'pokemons_captured' in playerdata and 'poke_stop_visits' in playerdata: + self.logger.info( + 'Pokemon Captured: ' + '{pokemons_captured}'.format( + **playerdata) + + ' | Pokestops Visited: ' + '{poke_stop_visits}'.format( + **playerdata)) + + def has_space_for_loot(self): + number_of_things_gained_by_stop = 5 + enough_space = ( + self.get_inventory_count('item') < + self._player['max_item_storage'] - number_of_things_gained_by_stop + ) + + return enough_space + + def get_forts(self, order_by_distance=False): + forts = [fort + for fort in self.cell['forts'] + if 'latitude' in fort and 'type' in fort] + + if order_by_distance: + forts.sort(key=lambda x: distance( + self.position[0], + self.position[1], + x['latitude'], + x['longitude'] + )) + + return forts + + 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.last_map_object = self.api.get_map_objects( + latitude=f2i(lat), + longitude=f2i(lng), + since_timestamp_ms=timestamp, + cell_id=cellid + ) + 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 new file mode 100644 index 0000000000..324d645043 --- /dev/null +++ b/pokemongo_bot/api_wrapper.py @@ -0,0 +1,157 @@ +import time +import logging + +from pgoapi.exceptions import (ServerSideRequestThrottlingException, + NotLoggedInException, ServerBusyOrOfflineException, + NoPlayerPositionSetException, EmptySubrequestChainException, + UnexpectedResponseException) +from pgoapi.pgoapi import PGoApi, PGoApiRequest, RpcApi +from pgoapi.protos.POGOProtos.Networking.Requests_pb2 import RequestType + +from human_behaviour import sleep + +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.logger = logging.getLogger(__name__) + self.request_callers = [] + self.last_api_request_time = None + self.requests_per_seconds = 2 + + 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): + if not result or result is None or not isinstance(result, dict): + return False + + if not 'responses' in result or not 'status_code' in result: + return False + + if not isinstance(result['responses'], dict): + return False + + # the response can still programatically be valid at this point + # but still be wrong. we need to check if the server did sent what we asked it + for request_caller in request_callers: + if not request_caller in result['responses']: + return False + + return True + + def call(self, max_retry=15): + request_callers = self._pop_request_callers() + if not self.can_call(): + return False # currently this is never ran, exceptions are raised before + + request_timestamp = None + api_req_method_list = self._req_method_list + result = None + try_cnt = 0 + throttling_retry = 0 + unexpected_response_retry = 0 + while True: + request_timestamp = self.throttle_sleep() + # self._call internally clear this field, so save it + self._req_method_list = [req_method for req_method in api_req_method_list] + should_throttle_retry = False + should_unexpected_response_retry = False + try: + result = self._call() + except ServerSideRequestThrottlingException: + should_throttle_retry = True + except UnexpectedResponseException: + should_unexpected_response_retry = True + + if should_throttle_retry: + throttling_retry += 1 + if throttling_retry >= max_retry: + raise ServerSideRequestThrottlingException('Server throttled too many times') + sleep(1) # huge sleep ? + continue # skip response checking + + if should_unexpected_response_retry: + unexpected_response_retry += 1 + if unexpected_response_retry >= 5: + self.logger.warning('Server is not responding correctly to our requests. Waiting for 30 seconds to reconnect.') + sleep(30) + else: + sleep(2) + continue + + if not self.is_response_valid(result, request_callers): + try_cnt += 1 + if try_cnt > 3: + self.logger.warning('Server seems to be busy or offline - try again - {}/{}'.format(try_cnt, max_retry)) + if try_cnt >= max_retry: + raise ServerBusyOrOfflineException() + sleep(1) + else: + break + + self.last_api_request_time = request_timestamp + return result + + def __getattr__(self, func): + if func.upper() in RequestType.keys(): + self.request_callers.append(func) + return PGoApiRequest.__getattr__(self, func) + + def throttle_sleep(self): + now_milliseconds = time.time() * 1000 + required_delay_between_requests = 1000 / self.requests_per_seconds + + 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: + sleep_time = required_delay_between_requests - difference + time.sleep(sleep_time / 1000) + + return now_milliseconds diff --git a/pokemongo_bot/cell_workers/__init__.py b/pokemongo_bot/cell_workers/__init__.py index 8f6653421c..bc6638d1fd 100644 --- a/pokemongo_bot/cell_workers/__init__.py +++ b/pokemongo_bot/cell_workers/__init__.py @@ -1,7 +1,21 @@ # -*- coding: utf-8 -*- +from catch_lured_pokemon import CatchLuredPokemon +from catch_visible_pokemon import CatchVisiblePokemon +from evolve_pokemon import EvolvePokemon +from incubate_eggs import IncubateEggs +from move_to_fort import MoveToFort +from move_to_map_pokemon import MoveToMapPokemon +from nickname_pokemon import NicknamePokemon from pokemon_catch_worker import PokemonCatchWorker -from seen_fort_worker import SeenFortWorker -from move_to_fort_worker import MoveToFortWorker -from initial_transfer_worker import InitialTransferWorker -from evolve_all_worker import EvolveAllWorker +from transfer_pokemon import TransferPokemon +from recycle_items import RecycleItems +from spin_fort import SpinFort +from handle_soft_ban import HandleSoftBan +from follow_path import FollowPath +from follow_spiral import FollowSpiral +from collect_level_up_reward import CollectLevelUpReward +from base_task import BaseTask +from follow_cluster import FollowCluster +from sleep_schedule import SleepSchedule +from update_title_stats import UpdateTitleStats \ No newline at end of file diff --git a/pokemongo_bot/cell_workers/base_task.py b/pokemongo_bot/cell_workers/base_task.py new file mode 100644 index 0000000000..ac48b9a676 --- /dev/null +++ b/pokemongo_bot/cell_workers/base_task.py @@ -0,0 +1,30 @@ +import logging + + +class BaseTask(object): + + 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 diff --git a/pokemongo_bot/cell_workers/catch_lured_pokemon.py b/pokemongo_bot/cell_workers/catch_lured_pokemon.py new file mode 100644 index 0000000000..bf2d45bb4b --- /dev/null +++ b/pokemongo_bot/cell_workers/catch_lured_pokemon.py @@ -0,0 +1,51 @@ +# -*- 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.cell_workers.base_task import BaseTask + + +class CatchLuredPokemon(BaseTask): + def work(self): + lured_pokemon = self.get_lured_pokemon() + if lured_pokemon: + self.catch_pokemon(lured_pokemon) + + def get_lured_pokemon(self): + forts = self.bot.get_forts(order_by_distance=True) + + if len(forts) == 0: + 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').encode('utf8', 'replace') + + encounter_id = fort.get('lure_info', {}).get('encounter_id', None) + + if encounter_id: + result = { + 'encounter_id': encounter_id, + 'fort_id': fort['id'], + 'fort_name': fort_name, + 'latitude': fort['latitude'], + 'longitude': fort['longitude'] + } + + self.emit_event( + 'lured_pokemon_found', + formatted='Lured pokemon at fort {fort_name} ({fort_id})', + data=result + ) + return result + + return False + + def catch_pokemon(self, pokemon): + worker = PokemonCatchWorker(pokemon, self.bot) + return_value = worker.work() + + return return_value diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py new file mode 100644 index 0000000000..c9c6147c0a --- /dev/null +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -0,0 +1,48 @@ +import json + +from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker +from utils import distance + + +class CatchVisiblePokemon(BaseTask): + def work(self): + if 'catchable_pokemons' in self.bot.cell and len(self.bot.cell['catchable_pokemons']) > 0: + # Sort all by distance from current pos- eventually this should + # build graph & A* it + self.bot.cell['catchable_pokemons'].sort( + key= + lambda x: distance(self.bot.position[0], self.bot.position[1], x['latitude'], x['longitude']) + ) + + 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', + data={ + 'pokemon_id': pokemon['pokemon_id'], + 'spawn_point_id': pokemon['spawn_point_id'], + 'encounter_id': pokemon['encounter_id'], + 'latitude': pokemon['latitude'], + 'longitude': pokemon['longitude'], + 'expiration_timestamp_ms': pokemon['expiration_timestamp_ms'], + } + ) + + return self.catch_pokemon(self.bot.cell['catchable_pokemons'].pop(0)) + + 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)) + + def catch_pokemon(self, pokemon): + worker = PokemonCatchWorker(pokemon, self.bot) + return_value = worker.work() + + return return_value diff --git a/pokemongo_bot/cell_workers/collect_level_up_reward.py b/pokemongo_bot/cell_workers/collect_level_up_reward.py new file mode 100644 index 0000000000..304818fe2b --- /dev/null +++ b/pokemongo_bot/cell_workers/collect_level_up_reward.py @@ -0,0 +1,74 @@ +from pokemongo_bot.cell_workers.base_task import BaseTask + + +class CollectLevelUpReward(BaseTask): + current_level = 0 + previous_level = 0 + + def initialize(self): + self.current_level = self._get_current_level() + self.previous_level = 0 + + def work(self): + self.current_level = self._get_current_level() + + # let's check level reward on bot initialization + # to be able get rewards for old bots + if self.previous_level == 0: + self._collect_level_reward() + # level up situation + elif self.current_level > self.previous_level: + self.emit_event( + 'level_up', + formatted='Level up from {previous_level} to {current_level}', + data={ + 'previous_level': self.previous_level, + 'current_level': self.current_level + } + ) + self._collect_level_reward() + + self.previous_level = self.current_level + + def _collect_level_reward(self): + 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', {}) + .get('LEVEL_UP_REWARDS', {}) + .get('items_awarded', [])) + + for item in data: + if 'item_id' in item and str(item['item_id']) in self.bot.item_list: + got_item = self.bot.item_list[str(item['item_id'])] + item['name'] = got_item + count = 'item_count' in item and item['item_count'] or 0 + + self.emit_event( + 'level_up_reward', + formatted='Received level up reward: {items}', + data={ + 'items': data + } + ) + + def _get_current_level(self): + level = 0 + response_dict = self.bot.get_inventory() + data = (response_dict + .get('responses', {}) + .get('GET_INVENTORY', {}) + .get('inventory_delta', {}) + .get('inventory_items', {})) + + for item in data: + level = (item + .get('inventory_item_data', {}) + .get('player_stats', {}) + .get('level', 0)) + + # we found a level, no need to continue iterate + if level: + break + + return level diff --git a/pokemongo_bot/cell_workers/evolve_all_worker.py b/pokemongo_bot/cell_workers/evolve_all_worker.py deleted file mode 100644 index 9c46dc7555..0000000000 --- a/pokemongo_bot/cell_workers/evolve_all_worker.py +++ /dev/null @@ -1,240 +0,0 @@ -from utils import distance, format_dist -from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot import logger -from sets import Set - -class EvolveAllWorker(object): - def __init__(self, bot): - self.api = bot.api - self.config = bot.config - self.bot = bot - # self.position = bot.position - - def work(self): - self.api.get_inventory() - response_dict = self.api.call() - cache = {} - - try: - reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) - except KeyError: - pass - else: - evolve_list = self._sort_by_cp(response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']) - if self.config.evolve_all[0] != 'all': - # filter out non-listed pokemons - evolve_list = [x for x in evolve_list if str(x[1]) in self.config.evolve_all] - - ## enable to limit number of pokemons to evolve. Useful for testing. - # nn = 1 - # if len(evolve_list) > nn: - # evolve_list = evolve_list[:nn] - ## - - id_list1 = self.count_pokemon_inventory() - for pokemon in evolve_list: - try: - self._execute_pokemon_evolve(pokemon, cache) - except: - pass - id_list2 = self.count_pokemon_inventory() - release_cand_list_ids = list(Set(id_list2) - Set(id_list1)) - - if release_cand_list_ids: - print('[#] Evolved {} pokemons! Checking if any of them needs to be released ...'.format( - len(release_cand_list_ids) - )) - self._release_evolved(release_cand_list_ids) - - def _release_evolved(self, release_cand_list_ids): - self.api.get_inventory() - response_dict = self.api.call() - cache = {} - - try: - reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) - except KeyError: - pass - else: - release_cand_list = self._sort_by_cp(response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']) - release_cand_list = [x for x in release_cand_list if x[0] in release_cand_list_ids] - - ## at this point release_cand_list contains evolved pokemons data - for cand in release_cand_list: - pokemon_id = cand[0] - pokemon_name = cand[1] - pokemon_cp = cand[2] - pokemon_potential = cand[3] - - if self.should_release_pokemon(pokemon_name, pokemon_cp, pokemon_potential): - # Transfering Pokemon - self.transfer_pokemon(pokemon_id) - logger.log( - '[#] {} has been exchanged for candy!'.format(pokemon_name), 'green') - - def _sort_by_cp(self, inventory_items): - pokemons = [] - for item in inventory_items: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data"], item) - except KeyError: - pass - else: - try: - pokemon = item['inventory_item_data']['pokemon_data'] - pokemon_num = int(pokemon['pokemon_id']) - 1 - pokemon_name = self.bot.pokemon_list[int(pokemon_num)]['Name'] - pokemons.append([ - pokemon['id'], - pokemon_name, - pokemon['cp'], - self._compute_iv(pokemon) - ]) - except: - pass - - pokemons.sort(key=lambda x: x[2], reverse=True) - return pokemons - - def _execute_pokemon_evolve(self, pokemon, cache): - pokemon_id = pokemon[0] - pokemon_name = pokemon[1] - pokemon_cp = pokemon[2] - - if pokemon_name in cache: - return - - self.api.evolve_pokemon(pokemon_id=pokemon_id) - response_dict = self.api.call() - status = response_dict['responses']['EVOLVE_POKEMON']['result'] - if status == 1: - print('[#] Successfully evolved {} with {} cp!'.format( - pokemon_name, pokemon_cp - )) - else: - # cache pokemons we can't evolve. Less server calls - cache[pokemon_name] = 1 - sleep(5.7) - - # TODO: move to utils. These methods are shared with other workers. - def transfer_pokemon(self, pid): - self.api.release_pokemon(pokemon_id=pid) - response_dict = self.api.call() - - def count_pokemon_inventory(self): - self.api.get_inventory() - response_dict = self.api.call() - id_list = [] - return self.counting_pokemon(response_dict, id_list) - - def counting_pokemon(self, response_dict, id_list): - try: - reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) - except KeyError: - pass - else: - for item in response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data"], item) - except KeyError: - pass - else: - pokemon = item['inventory_item_data']['pokemon_data'] - if pokemon.get('is_egg', False): - continue - id_list.append(pokemon['id']) - - return id_list - - def should_release_pokemon(self, pokemon_name, cp, iv): - if self._check_always_capture_exception_for(pokemon_name): - return False - else: - release_config = self._get_release_config_for(pokemon_name) - cp_iv_logic = release_config.get('cp_iv_logic') - if not cp_iv_logic: - cp_iv_logic = self._get_release_config_for('any').get('cp_iv_logic', 'and') - - release_results = { - 'cp': False, - 'iv': False, - } - - if 'release_under_cp' in release_config: - min_cp = release_config['release_under_cp'] - if cp < min_cp: - release_results['cp'] = True - - if 'release_under_iv' in release_config: - min_iv = release_config['release_under_iv'] - if iv < min_iv: - release_results['iv'] = True - - if release_config.get('always_release'): - return True - - logic_to_function = { - 'or': lambda x, y: x or y, - 'and': lambda x, y: x and y - } - - #logger.log( - # "[x] Release config for {}: CP {} {} IV {}".format( - # pokemon_name, - # min_cp, - # cp_iv_logic, - # min_iv - # ), 'yellow' - #) - - return logic_to_function[cp_iv_logic](*release_results.values()) - - def _get_release_config_for(self, pokemon): - release_config = self.config.release_config.get(pokemon) - if not release_config: - release_config = self.config.release_config['any'] - return release_config - - def _get_exceptions(self): - exceptions = self.config.release_config.get('exceptions') - if not exceptions: - return None - return exceptions - - def _get_always_capture_list(self): - exceptions = self._get_exceptions() - if not exceptions: - return [] - always_capture_list = exceptions['always_capture'] - if not always_capture_list: - return [] - return always_capture_list - - def _check_always_capture_exception_for(self, pokemon_name): - always_capture_list = self._get_always_capture_list() - if not always_capture_list: - return False - else: - for pokemon in always_capture_list: - if pokemon_name == str(pokemon): - return True - return False - - # TODO: should also go to util and refactor in catch worker - def _compute_iv(self, pokemon): - total_IV = 0.0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - - for individual_stat in iv_stats: - try: - total_IV += pokemon[individual_stat] - except: - pokemon[individual_stat] = 0 - continue - pokemon_potential = round((total_IV / 45.0), 2) - return pokemon_potential diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py new file mode 100644 index 0000000000..911d6a1f67 --- /dev/null +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -0,0 +1,168 @@ +from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.item_list import Item +from pokemongo_bot.cell_workers.base_task import BaseTask + + +class EvolvePokemon(BaseTask): + + def initialize(self): + self.api = self.bot.api + self.evolve_all = self.config.get('evolve_all', []) + self.evolve_speed = self.config.get('evolve_speed', 2) + self.first_evolve_by = self.config.get('first_evolve_by', 'cp') + self.evolve_above_cp = self.config.get('evolve_above_cp', 500) + self.evolve_above_iv = self.config.get('evolve_above_iv', 0.8) + self.cp_iv_logic = self.config.get('logic', 'or') + self.use_lucky_egg = self.config.get('use_lucky_egg', False) + self._validate_config() + + def _validate_config(self): + if isinstance(self.evolve_all, basestring): + self.evolve_all = [str(pokemon_name).strip() for pokemon_name in self.evolve_all.split(',')] + + 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) + + if self.evolve_all[0] != 'all': + # filter out non-listed pokemons + 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) + + def _should_run(self): + if not self.evolve_all or self.evolve_all[0] == 'none': + return False + + # Evolve all is used - Use Lucky egg only at the first tick + if self.bot.tick_count is not 1 or not self.use_lucky_egg: + return True + + lucky_egg_count = self.bot.item_inventory_count(Item.ITEM_LUCKY_EGG.value) + + # Make sure the user has a lucky egg and skip if not + if lucky_egg_count > 0: + response_dict_lucky_egg = self.bot.use_lucky_egg() + if response_dict_lucky_egg: + result = response_dict_lucky_egg.get('responses', {}).get('USE_ITEM_XP_BOOST', {}).get('result', 0) + if result is 1: # Request success + self.emit_event( + 'used_lucky_egg', + formmated='Used lucky egg ({amount_left} left).', + data={ + 'amount_left': lucky_egg_count - 1 + } + ) + return True + else: + self.emit_event( + 'lucky_egg_error', + level='error', + formatted='Failed to use lucky egg!' + ) + return False + else: + # Skipping evolve so they aren't wasted + self.emit_event( + 'skip_evolve', + formatted='Skipping evolve because has no lucky egg.' + ) + 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): + 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 + } + 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)): + pokemons.append(pokemon) + + if self.first_evolve_by == "cp": + pokemons.sort(key=lambda x: (x['num'], x["cp"], x["iv"]), reverse=True) + else: + pokemons.sort(key=lambda x: (x['num'], 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: + return False + + 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 + } + ) + candy_list[pokemon["candies_family"]] -= pokemon["candies_amount"] + sleep(self.evolve_speed) + return True + else: + # cache pokemons we can't evolve. Less server calls + cache[pokemon_name] = 1 + sleep(0.7) + return False + + def _compute_iv(self, pokemon): + total_iv = pokemon.get("individual_attack", 0) + pokemon.get("individual_stamina", 0) + pokemon.get( + "individual_defense", 0) + return round((total_iv / 45.0), 2) diff --git a/pokemongo_bot/cell_workers/follow_cluster.py b/pokemongo_bot/cell_workers/follow_cluster.py new file mode 100644 index 0000000000..02d3880a7e --- /dev/null +++ b/pokemongo_bot/cell_workers/follow_cluster.py @@ -0,0 +1,85 @@ +from pokemongo_bot.step_walker import StepWalker +from pokemongo_bot.cell_workers.utils import distance +from pokemongo_bot.cell_workers.utils import find_biggest_cluster +from pokemongo_bot.cell_workers.base_task import BaseTask + +class FollowCluster(BaseTask): + + def initialize(self): + self.is_at_destination = False + self.announced = False + self.dest = None + self._process_config() + + def _process_config(self): + self.lured = self.config.get("lured", True) + self.radius = self.config.get("radius", 50) + + def work(self): + forts = self.bot.get_forts() + log_lure_avail_str = '' + log_lured_str = '' + if self.lured: + log_lured_str = 'lured ' + lured_forts = [x for x in forts if 'lure_info' in x] + if len(lured_forts) > 0: + self.dest = find_biggest_cluster(self.radius, lured_forts, 'lure_info') + else: + log_lure_avail_str = 'No lured pokestops in vicinity. Search for normal ones instead. ' + self.dest = find_biggest_cluster(self.radius, forts) + else: + self.dest = find_biggest_cluster(self.radius, forts) + + if self.dest is not None: + + lat = self.dest['latitude'] + lng = self.dest['longitude'] + cnt = self.dest['num_points'] + + if not self.is_at_destination: + msg = log_lure_avail_str + ( + "Move to destiny {num_points}. {forts} " + "pokestops will be in range of {radius}. Walking {distance}m." + ) + self.emit_event( + 'found_cluster', + formatted=msg, + data={ + 'num_points': cnt, + 'forts': log_lured_str, + 'radius': str(self.radius), + 'distance': str(distance(self.bot.position[0], self.bot.position[1], lat, lng)) + } + ) + + self.announced = False + + if self.bot.config.walk > 0: + step_walker = StepWalker( + self.bot, + self.bot.config.walk, + lat, + lng + ) + + self.is_at_destination = False + if step_walker.step(): + self.is_at_destination = True + else: + self.bot.api.set_position(lat, lng) + + elif not self.announced: + self.emit_event( + 'arrived_at_cluster', + formatted="Arrived at cluster. {forts} are in a range of {radius}m radius.", + data={ + 'forts': str(cnt), + 'radius': self.radius + } + ) + self.announced = True + else: + lat = self.bot.position[0] + lng = self.bot.position[1] + + return [lat, lng] diff --git a/pokemongo_bot/cell_workers/follow_path.py b/pokemongo_bot/cell_workers/follow_path.py new file mode 100644 index 0000000000..04eb817593 --- /dev/null +++ b/pokemongo_bot/cell_workers/follow_path.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +import gpxpy +import gpxpy.gpx +import json +from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.cell_workers.utils import distance, i2f, format_dist +from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.step_walker import StepWalker +from pgoapi.utilities import f2i + + +class FollowPath(BaseTask): + def initialize(self): + self.ptr = 0 + self._process_config() + self.points = self.load_path() + + def _process_config(self): + self.path_file = self.config.get("path_file", None) + self.path_mode = self.config.get("path_mode", "linear") + + def load_path(self): + if self.path_file is None: + raise RuntimeError('You need to specify a path file (json or gpx)') + + if self.path_file.endswith('.json'): + return self.load_json() + elif self.path_file.endswith('.gpx'): + return self.load_gpx() + + def load_json(self): + with open(self.path_file) as data_file: + points=json.load(data_file) + # Replace Verbal Location with lat&lng. + for index, point in enumerate(points): + point_tuple = self.bot.get_pos_by_name(point['location']) + self.emit_event( + 'location_found', + level='debug', + formatted="Location found: {location} {position}", + data={ + 'location': point, + 'position': point_tuple + } + ) + points[index] = self.lat_lng_tuple_to_dict(point_tuple) + return points + + def lat_lng_tuple_to_dict(self, tpl): + return {'lat': tpl[0], 'lng': tpl[1]} + + def load_gpx(self): + gpx_file = open(self.path_file, 'r') + gpx = gpxpy.parse(gpx_file) + + if len(gpx.tracks) == 0: + raise RuntimeError('GPX file does not cotain a track') + + points = [] + track = gpx.tracks[0] + for segment in track.segments: + for point in segment.points: + points.append({"lat": point.latitude, "lng": point.longitude}) + + return points + + def work(self): + point = self.points[self.ptr] + lat = float(point['lat']) + lng = float(point['lng']) + + if self.bot.config.walk > 0: + step_walker = StepWalker( + self.bot, + self.bot.config.walk, + lat, + lng + ) + + is_at_destination = False + if step_walker.step(): + is_at_destination = True + + else: + self.bot.api.set_position(lat, lng) + + dist = distance( + self.bot.api._position_lat, + self.bot.api._position_lng, + lat, + lng + ) + + if dist <= 1 or (self.bot.config.walk > 0 and is_at_destination): + if (self.ptr + 1) == len(self.points): + self.ptr = 0 + if self.path_mode == 'linear': + self.points = list(reversed(self.points)) + else: + self.ptr += 1 + + return [lat, lng] diff --git a/pokemongo_bot/cell_workers/follow_spiral.py b/pokemongo_bot/cell_workers/follow_spiral.py new file mode 100644 index 0000000000..28b548d1ca --- /dev/null +++ b/pokemongo_bot/cell_workers/follow_spiral.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import math + +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.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, 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): + """ + Sourced from: + https://github.com/tejado/pgoapi/blob/master/examples/spiral_poi_search.py + + :param starting_lat: + :param starting_lng: + :param step_size: + :param step_limit: + :return: + """ + 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_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_lat + starting_lat + lng = y * step_size_lng + starting_lng + coords.append({'lat': lat, 'lng': lng}) + + d *= -1 + m += 1 + return coords + + def work(self): + point = self.points[self.ptr] + self.cnt += 1 + + if self.bot.config.walk > 0: + step_walker = StepWalker( + self.bot, + self.bot.config.walk, + point['lat'], + point['lng'] + ) + + dist = distance( + self.bot.api._position_lat, + self.bot.api._position_lng, + point['lat'], + point['lng'] + ) + + if self.cnt == 1: + self.emit_event( + 'position_update', + formatted="Walking from {last_position} to {current_position} ({distance} {distance_unit})", + data={ + 'last_position': self.bot.position, + 'current_position': (point['lat'], point['lng'], 0), + 'distance': dist, + 'distance_unit': 'm' + } + ) + + if step_walker.step(): + step_walker = None + else: + self.bot.api.set_position(point['lat'], point['lng']) + + if distance( + self.bot.api._position_lat, + self.bot.api._position_lng, + 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: + self.direction *= -1 + 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 new file mode 100644 index 0000000000..e266c5c377 --- /dev/null +++ b/pokemongo_bot/cell_workers/handle_soft_ban.py @@ -0,0 +1,69 @@ +from random import randint + +from pgoapi.utilities import f2i + +from pokemongo_bot.constants import Constants +from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.cell_workers import MoveToFort +from pokemongo_bot.cell_workers.utils import distance +from pokemongo_bot.worker_result import WorkerResult + + +class HandleSoftBan(BaseTask): + def work(self): + if not self.should_run(): + return + + forts = self.bot.get_forts(order_by_distance=True) + + if len(forts) == 0: + return + + fort_distance = distance( + self.bot.position[0], + self.bot.position[1], + forts[0]['latitude'], + forts[0]['longitude'], + ) + + if fort_distance > Constants.MAX_DISTANCE_FORT_IS_REACHABLE: + MoveToFort(self.bot, config=None).work() + self.bot.recent_forts = self.bot.recent_forts[0:-1] + if forts[0]['id'] in self.bot.fort_timeouts: + del self.bot.fort_timeouts[forts[0]['id']] + return WorkerResult.RUNNING + else: + spins = randint(50,60) + self.emit_event( + 'softban_fix', + formatted='Fixing softban.' + ) + for i in xrange(spins): + self.spin_fort(forts[0]) + self.bot.softban = False + self.emit_event( + 'softban_fix_done', + formatted='Softban should be fixed' + ) + + def spin_fort(self, fort): + self.bot.api.fort_search( + fort_id=fort['id'], + fort_latitude=fort['latitude'], + fort_longitude=fort['longitude'], + player_latitude=f2i(self.bot.position[0]), + player_longitude=f2i(self.bot.position[1]) + ) + self.bot.event_handler.emit( + 'spun_fort', + level='debug', + formatted="Spun fort {fort_id}", + data={ + 'fort_id': fort_id, + 'lat': fort['latitude'], + 'lng': fort['longitude'] + } + ) + + 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 new file mode 100644 index 0000000000..9e21b0d280 --- /dev/null +++ b/pokemongo_bot/cell_workers/incubate_eggs.py @@ -0,0 +1,207 @@ +from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.cell_workers.base_task import BaseTask + + +class IncubateEggs(BaseTask): + last_km_walked = 0 + + def initialize(self): + self.ready_incubators = [] + self.used_incubators = [] + self.eggs = [] + self.km_walked = 0 + self.hatching_animation_delay = 4.20 + self.max_iv = 45.0 + + self._process_config() + + def _process_config(self): + self.longer_eggs_first = self.config.get("longer_eggs_first", True) + + def work(self): + try: + self._check_inventory() + except: + return + + if self.used_incubators and IncubateEggs.last_km_walked != self.km_walked: + self.used_incubators.sort(key=lambda x: x.get("km")) + km_left = self.used_incubators[0]['km']-self.km_walked + if km_left <= 0: + self._hatch_eggs() + else: + self.emit_event( + 'next_egg_incubates', + formatted='Next egg incubates in {distance_in_km:.2f} km', + data={ + 'distance_in_km': km_left + } + ) + IncubateEggs.last_km_walked = self.km_walked + + sorting = self.longer_eggs_first + self.eggs.sort(key=lambda x: x.get("km"), reverse=sorting) + + if self.ready_incubators: + self._apply_incubators() + + def _apply_incubators(self): + for incubator in self.ready_incubators: + for egg in self.eggs: + if egg["used"] or egg["km"] == -1: + continue + self.emit_event( + 'incubate_try', + level='debug', + formatted="Attempting to apply incubator {incubator_id} to egg {egg_id}", + data={ + 'incubator_id': incubator['id'], + 'egg_id': egg['id'] + } + ) + 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: + self.emit_event( + 'incubate', + formatted='Incubating a {distance_in_km} egg.', + data={ + 'distance_in_km': str(egg['km']) + } + ) + egg["used"] = True + incubator["used"] = True + break + elif code == 5 or code == 7: + self.emit_event( + 'incubator_already_used', + level='debug', + formatted='Incubator in use.', + ) + incubator["used"] = True + break + elif code == 6: + self.emit_event( + 'egg_already_incubating', + level='debug', + formatted='Egg already incubating', + ) + egg["used"] = True + + def _check_inventory(self, lookup_ids=[]): + inv = {} + response_dict = self.bot.get_inventory() + matched_pokemon = [] + temp_eggs = [] + temp_used_incubators = [] + temp_ready_incubators = [] + inv = reduce( + dict.__getitem__, + ["responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], + response_dict + ) + for inv_data in inv: + inv_data = inv_data.get("inventory_item_data", {}) + if "egg_incubators" in inv_data: + temp_used_incubators = [] + temp_ready_incubators = [] + incubators = inv_data.get("egg_incubators", {}).get("egg_incubator",[]) + if isinstance(incubators, basestring): # checking for old response + incubators = [incubators] + for incubator in incubators: + if 'pokemon_id' in incubator: + temp_used_incubators.append({ + "id": incubator.get('id', -1), + "km": incubator.get('target_km_walked', 9001) + }) + else: + temp_ready_incubators.append({ + "id": incubator.get('id', -1) + }) + continue + if "pokemon_data" in inv_data: + pokemon = inv_data.get("pokemon_data", {}) + if pokemon.get("is_egg", False) and "egg_incubator_id" not in pokemon: + temp_eggs.append({ + "id": pokemon.get("id", -1), + "km": pokemon.get("egg_km_walked_target", -1), + "used": False + }) + elif 'is_egg' not in pokemon and pokemon['id'] in lookup_ids: + pokemon.update({ + "iv": [ + pokemon.get('individual_attack', 0), + pokemon.get('individual_defense', 0), + pokemon.get('individual_stamina', 0) + ]}) + matched_pokemon.append(pokemon) + continue + if "player_stats" in inv_data: + self.km_walked = inv_data.get("player_stats", {}).get("km_walked", 0) + if temp_used_incubators: + self.used_incubators = temp_used_incubators + if temp_ready_incubators: + self.ready_incubators = temp_ready_incubators + if temp_eggs: + self.eggs = temp_eggs + return matched_pokemon + + def _hatch_eggs(self): + response_dict = self.bot.api.get_hatched_eggs() + log_color = 'green' + try: + result = reduce(dict.__getitem__, ["responses", "GET_HATCHED_EGGS"], response_dict) + except KeyError: + return + pokemon_ids = [] + if 'pokemon_id' in result: + pokemon_ids = [id for id in result['pokemon_id']] + stardust = result.get('stardust_awarded', "error") + candy = result.get('candy_awarded', "error") + xp = result.get('experience_awarded', "error") + sleep(self.hatching_animation_delay) + self.bot.latest_inventory = None + try: + pokemon_data = self._check_inventory(pokemon_ids) + for pokemon in pokemon_data: + # pokemon ids seem to be offset by one + if pokemon['pokemon_id']!=-1: + pokemon['name'] = self.bot.pokemon_list[(pokemon.get('pokemon_id')-1)]['Name'] + else: + pokemon['name'] = "error" + except: + pokemon_data = [{"name":"error","cp":"error","iv":"error"}] + if not pokemon_ids or pokemon_data[0]['name'] == "error": + self.emit_event( + 'egg_hatched', + data={ + 'pokemon': 'error', + 'cp': 'error', + 'iv': 'error', + 'exp': 'error', + 'stardust': 'error', + 'candy': 'error', + } + ) + return + for i in range(len(pokemon_data)): + msg = "Egg hatched with a {pokemon} (CP {cp} - IV {iv}), {exp} exp, {stardust} stardust and {candy} candies." + self.emit_event( + 'egg_hatched', + formatted=msg, + data={ + 'pokemon': pokemon_data[i]['name'], + 'cp': pokemon_data[i]['cp'], + 'iv': "{} {}".format( + "/".join(map(str, pokemon_data[i]['iv'])), + sum(pokemon_data[i]['iv'])/self.max_iv + ), + 'exp': xp[i], + 'stardust': stardust[i], + 'candy': candy[i], + } + ) diff --git a/pokemongo_bot/cell_workers/initial_transfer_worker.py b/pokemongo_bot/cell_workers/initial_transfer_worker.py deleted file mode 100644 index 026e79ddaa..0000000000 --- a/pokemongo_bot/cell_workers/initial_transfer_worker.py +++ /dev/null @@ -1,75 +0,0 @@ -import json - -from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot import logger - -class InitialTransferWorker(object): - def __init__(self, bot): - self.config = bot.config - self.pokemon_list = bot.pokemon_list - self.api = bot.api - - def work(self): - logger.log('[x] Initial Transfer.') - - logger.log( - '[x] Preparing to transfer all duplicate Pokemon, keeping the highest CP of each type.') - - logger.log('[x] Will NOT transfer anything above CP {}'.format( - self.config.initial_transfer)) - - pokemon_groups = self._initial_transfer_get_groups() - - for id in pokemon_groups: - - group_cp = pokemon_groups[id].keys() - - if len(group_cp) > 1: - group_cp.sort() - group_cp.reverse() - - - for x in range(1, len(group_cp)): - if self.config.initial_transfer and group_cp[x] > self.config.initial_transfer: - continue - - print('[x] Transferring {} with CP {}'.format( - self.pokemon_list[id - 1]['Name'], group_cp[x])) - self.api.release_pokemon( - pokemon_id=pokemon_groups[id][group_cp[x]]) - response_dict = self.api.call() - sleep(2) - - logger.log('[x] Transferring Done.') - - def _initial_transfer_get_groups(self): - pokemon_groups = {} - self.api.get_player().get_inventory() - inventory_req = self.api.call() - inventory_dict = inventory_req['responses']['GET_INVENTORY'][ - 'inventory_delta']['inventory_items'] - - user_web_inventory = 'web/inventory-%s.json' % (self.config.username) - with open(user_web_inventory, 'w') as outfile: - json.dump(inventory_dict, outfile) - - for pokemon in inventory_dict: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data", "pokemon_id" - ], pokemon) - except KeyError: - continue - - group_id = pokemon['inventory_item_data'][ - 'pokemon_data']['pokemon_id'] - group_pokemon = pokemon['inventory_item_data'][ - 'pokemon_data']['id'] - group_pokemon_cp = pokemon[ - 'inventory_item_data']['pokemon_data']['cp'] - - if group_id not in pokemon_groups: - pokemon_groups[group_id] = {} - - pokemon_groups[group_id].update({group_pokemon_cp: group_pokemon}) - return pokemon_groups diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py new file mode 100644 index 0000000000..f43d1641e6 --- /dev/null +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from pokemongo_bot.constants import Constants +from pokemongo_bot.step_walker import StepWalker +from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.cell_workers.base_task import BaseTask +from utils import distance, format_dist, fort_details + + +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) + + 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." + ) + return has_space_for_loot or self.bot.softban + + def is_attracted(self): + return (self.lure_distance > 0) + + def work(self): + if not self.should_run(): + return WorkerResult.SUCCESS + + nearest_fort = self.get_nearest_fort() + + if nearest_fort is None: + return WorkerResult.SUCCESS + + lat = nearest_fort['latitude'] + lng = nearest_fort['longitude'] + fortID = nearest_fort['id'] + details = fort_details(self.bot, fortID, lat, lng) + fort_name = details.get('name', 'Unknown') + + unit = self.bot.config.distance_unit # Unit to use when printing formatted distance + + dist = distance( + self.bot.position[0], + self.bot.position[1], + lat, + lng + ) + + if dist > Constants.MAX_DISTANCE_FORT_IS_REACHABLE: + fort_event_data = { + 'fort_name': u"{}".format(fort_name), + 'distance': format_dist(dist, unit), + } + + if self.is_attracted() > 0: + fort_event_data.update(lure_distance=format_dist(self.lure_distance, unit)) + self.emit_event( + 'moving_to_lured_fort', + formatted="Moving towards pokestop {fort_name} - {distance} (attraction of lure {lure_distance})", + data=fort_event_data + ) + else: + self.emit_event( + 'moving_to_fort', + formatted="Moving towards pokestop {fort_name} - {distance}", + data=fort_event_data + ) + + step_walker = StepWalker( + self.bot, + self.bot.config.walk, + lat, + lng + ) + + if not step_walker.step(): + return WorkerResult.RUNNING + + self.emit_event( + 'arrived_at_fort', + formatted='Arrived at fort.' + ) + return WorkerResult.SUCCESS + + def _get_nearest_fort_on_lure_way(self, forts): + + if not self.lure_attraction: + return None, 0 + + lures = filter(lambda x: True if x.get('lure_info', None) != None else False, forts) + + if (len(lures)): + dist_lure_me = distance(self.bot.position[0], self.bot.position[1], + lures[0]['latitude'],lures[0]['longitude']) + else: + dist_lure_me = 0 + + if dist_lure_me > 0 and dist_lure_me < self.lure_max_distance: + + self.lure_distance = dist_lure_me + + for fort in forts: + dist_lure_fort = distance( + fort['latitude'], + fort['longitude'], + lures[0]['latitude'], + lures[0]['longitude']) + dist_fort_me = distance( + fort['latitude'], + fort['longitude'], + self.bot.position[0], + self.bot.position[1]) + + if dist_lure_fort < dist_lure_me and dist_lure_me > dist_fort_me: + return fort, dist_lure_me + + if dist_fort_me > dist_lure_me: + break + + return lures[0], dist_lure_me + + else: + return None, 0 + + def get_nearest_fort(self): + forts = self.bot.get_forts(order_by_distance=True) + + # Remove stops that are still on timeout + forts = filter(lambda x: x["id"] not in self.bot.fort_timeouts, forts) + + next_attracted_pts, lure_distance = self._get_nearest_fort_on_lure_way(forts) + + # Remove all forts which were spun in the last ticks to avoid circles if set + if self.bot.config.forts_avoid_circles: + forts = filter(lambda x: x["id"] not in self.bot.recent_forts, forts) + + self.lure_distance = lure_distance + + if (lure_distance > 0): + return next_attracted_pts + + if len(forts) > 0: + return forts[0] + else: + return None diff --git a/pokemongo_bot/cell_workers/move_to_fort_worker.py b/pokemongo_bot/cell_workers/move_to_fort_worker.py deleted file mode 100644 index 3af7befcd3..0000000000 --- a/pokemongo_bot/cell_workers/move_to_fort_worker.py +++ /dev/null @@ -1,40 +0,0 @@ -from utils import distance, format_dist -from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot import logger - -class MoveToFortWorker(object): - def __init__(self, fort, bot): - self.fort = fort - self.api = bot.api - self.config = bot.config - self.stepper = bot.stepper - self.position = bot.position - - def work(self): - lat = self.fort['latitude'] - lng = self.fort['longitude'] - fortID = self.fort['id'] - unit = self.config.distance_unit # Unit to use when printing formatted distance - - dist = distance(self.position[0], self.position[1], lat, lng) - - # print('[#] Found fort {} at distance {}m'.format(fortID, dist)) - logger.log('[#] Found fort {} at distance {}'.format( - fortID, format_dist(dist, unit))) - - if dist > 10: - logger.log('[#] Need to move closer to Pokestop') - position = (lat, lng, 0.0) - - if self.config.walk > 0: - self.stepper._walk_to(self.config.walk, *position) - else: - self.api.set_position(*position) - - self.api.player_update(latitude=lat, longitude=lng) - response_dict = self.api.call() - logger.log('[#] Arrived at Pokestop') - sleep(2) - return response_dict - - return None diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py new file mode 100644 index 0000000000..bce39e0143 --- /dev/null +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- + +import os +import time +import json +import base64 +import requests +from pokemongo_bot import logger +from pokemongo_bot.cell_workers.utils import distance, format_dist, format_time +from pokemongo_bot.step_walker import StepWalker +from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker + + +class MoveToMapPokemon(BaseTask): + def initialize(self): + self.last_map_update = 0 + self.pokemon_data = self.bot.pokemon_list + self.unit = self.bot.config.distance_unit + self.caught = [] + + data_file = 'data/map-caught-{}.json'.format(self.bot.config.username) + if os.path.isfile(data_file): + self.caught = json.load( + open(data_file) + ) + + def get_pokemon_from_map(self): + try: + req = requests.get('{}/raw_data?gyms=false&scanned=false'.format(self.config['address'])) + except requests.exceptions.ConnectionError: + logger.log('Could not reach PokemonGo-Map Server', 'red') + return [] + + try: + raw_data = req.json() + except ValueError: + logger.log('Map data was not valid', 'red') + return [] + + pokemon_list = [] + now = int(time.time()) + + for pokemon in raw_data['pokemons']: + try: + pokemon['encounter_id'] = long(base64.b64decode(pokemon['encounter_id'])) + except TypeError: + log.logger('base64 error: {}'.format(pokemon['encounter_id']), 'red') + continue + pokemon['spawn_point_id'] = pokemon['spawnpoint_id'] + pokemon['disappear_time'] = int(pokemon['disappear_time'] / 1000) + pokemon['name'] = self.pokemon_data[pokemon['pokemon_id'] - 1]['Name'] + pokemon['is_vip'] = pokemon['name'] in self.bot.config.vips + + if pokemon['name'] not in self.config['catch'] and not pokemon['is_vip']: + continue + + if pokemon['disappear_time'] < (now + self.config['min_time']): + continue + + if self.was_caught(pokemon): + continue + + pokemon['priority'] = self.config['catch'].get(pokemon['name'], 0) + + pokemon['dist'] = distance( + self.bot.position[0], + self.bot.position[1], + pokemon['latitude'], + pokemon['longitude'], + ) + + if pokemon['dist'] > self.config['max_distance'] and not self.config['snipe']: + continue + + pokemon_list.append(pokemon) + + return pokemon_list + + def add_caught(self, pokemon): + for caught_pokemon in self.caught: + if caught_pokemon['encounter_id'] == pokemon['encounter_id']: + return + if len(self.caught) >= 200: + self.caught.pop(0) + self.caught.append(pokemon) + + def was_caught(self, pokemon): + for caught_pokemon in self.caught: + if pokemon['encounter_id'] == caught_pokemon['encounter_id']: + return True + return False + + def update_map_location(self): + if not self.config['update_map']: + return + try: + req = requests.get('{}/loc'.format(self.config['address'])) + except requests.exceptions.ConnectionError: + logger.log('Could not reach PokemonGo-Map Server', 'red') + return + + try: + loc_json = req.json() + except ValueError: + return log.logger('Map location data was not valid', 'red') + + + dist = distance( + self.bot.position[0], + self.bot.position[1], + loc_json['lat'], + loc_json['lng'] + ) + + # update map when 500m away from center and last update longer than 2 minutes away + now = int(time.time()) + if dist > 500 and now - self.last_map_update > 2 * 60: + requests.post('{}/next_loc?lat={}&lon={}'.format(self.config['address'], self.bot.position[0], self.bot.position[1])) + logger.log('Updated PokemonGo-Map position') + self.last_map_update = now + + def snipe(self, pokemon): + last_position = self.bot.position[0:2] + + self.bot.heartbeat() + + logger.log('Teleporting to {} ({})'.format(pokemon['name'], format_dist(pokemon['dist'], self.unit)), 'green') + self.bot.api.set_position(pokemon['latitude'], pokemon['longitude'], 0) + + logger.log('Encounter pokemon', 'green') + catch_worker = PokemonCatchWorker(pokemon, self.bot) + api_encounter_response = catch_worker.create_encounter_api_call() + + time.sleep(2) + logger.log('Teleporting back to previous location..', 'green') + self.bot.api.set_position(last_position[0], last_position[1], 0) + time.sleep(2) + self.bot.heartbeat() + + catch_worker.work(api_encounter_response) + self.add_caught(pokemon) + + return WorkerResult.SUCCESS + + def dump_caught_pokemon(self): + user_data_map_caught = 'data/map-caught-{}.json'.format(self.bot.config.username) + with open(user_data_map_caught, 'w') as outfile: + json.dump(self.caught, outfile) + + def work(self): + # check for pokeballs (excluding masterball) + pokeballs = self.bot.item_inventory_count(1) + superballs = self.bot.item_inventory_count(2) + ultraballs = self.bot.item_inventory_count(3) + + if (pokeballs + superballs + ultraballs) < 1: + return WorkerResult.SUCCESS + + self.update_map_location() + self.dump_caught_pokemon() + + pokemon_list = self.get_pokemon_from_map() + pokemon_list.sort(key=lambda x: x['dist']) + if self.config['mode'] == 'priority': + pokemon_list.sort(key=lambda x: x['priority'], reverse=True) + if self.config['prioritize_vips']: + pokemon_list.sort(key=lambda x: x['is_vip'], reverse=True) + + if len(pokemon_list) < 1: + return WorkerResult.SUCCESS + + pokemon = pokemon_list[0] + + # if we only have ultraballs and the target is not a vip don't snipe/walk + if (pokeballs + superballs) < 1 and not pokemon['is_vip']: + return WorkerResult.SUCCESS + + if self.config['snipe']: + return self.snipe(pokemon) + + now = int(time.time()) + logger.log('Moving towards {}, {} left ({})'.format(pokemon['name'], format_dist(pokemon['dist'], self.unit), format_time(pokemon['disappear_time'] - now))) + step_walker = StepWalker( + self.bot, + self.bot.config.walk, + pokemon['latitude'], + pokemon['longitude'] + ) + + if not step_walker.step(): + return WorkerResult.RUNNING + + logger.log('Arrived at {}'.format(pokemon['name'])) + self.add_caught(pokemon) + return WorkerResult.SUCCESS diff --git a/pokemongo_bot/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py new file mode 100644 index 0000000000..29df15ae4a --- /dev/null +++ b/pokemongo_bot/cell_workers/nickname_pokemon.py @@ -0,0 +1,101 @@ +from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.cell_workers.base_task import BaseTask + +class NicknamePokemon(BaseTask): + 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()) + except KeyError: + pass + else: + pokemon_data = self._get_inventory_pokemon(inventory) + for pokemon in pokemon_data: + self._nickname_pokemon(pokemon) + + def _get_inventory_pokemon(self,inventory_dict): + pokemon_data = [] + for inv_data in inventory_dict: + try: + pokemon = reduce(dict.__getitem__,['inventory_item_data','pokemon_data'],inv_data) + except KeyError: + pass + else: + 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 = "" + instance_id = pokemon.get('id',0) + if not instance_id: + self.emit_event( + 'api_error', + formatted='Failed to get pokemon name, will not rename.' + ) + return + id = pokemon.get('pokemon_id',0)-1 + name = self.bot.pokemon_list[id]['Name'] + cp = pokemon.get('cp',0) + iv_attack = pokemon.get('individual_attack',0) + iv_defense = pokemon.get('individual_defense',0) + iv_stamina = pokemon.get('individual_stamina',0) + iv_list = [iv_attack,iv_defense,iv_stamina] + iv_ads = "/".join(map(str,iv_list)) + iv_sum = sum(iv_list) + iv_pct = "{:0.0f}".format(100*iv_sum/45.0) + log_color = 'red' + try: + new_name = self.template.format(name=name, + id=id, + cp=cp, + iv_attack=iv_attack, + iv_defense=iv_defense, + iv_stamina=iv_stamina, + iv_ads=iv_ads, + iv_sum=iv_sum, + iv_pct=iv_pct)[:12] + except KeyError as bad_key: + self.emit_event( + 'config_error', + formatted="Unable to nickname {} due to bad template ({})".format(name,bad_key) + ) + if pokemon.get('nickname', '') == new_name: + return + response = self.bot.api.nickname_pokemon(pokemon_id=instance_id,nickname=new_name) + sleep(1.2) + try: + result = reduce(dict.__getitem__, ["responses", "NICKNAME_POKEMON"], response) + except KeyError: + self.emit_event( + 'api_error', + formatted='Attempt to nickname received bad response from server.' + ) + result = result['result'] + new_name = new_name or name + if result == 0: + self.emit_event( + 'unset_pokemon_nickname', + formatted="Pokemon nickname unset." + ) + elif result == 1: + self.emit_event( + 'rename_pokemon', + formatted="Pokemon {old_name} renamed to {current_name}", + data={ + 'old_name': name, + 'current_name': new_name + } + ) + pokemon['nickname'] = new_name + elif result == 2: + self.emit_event( + 'pokemon_nickname_invalid', + formatted="Nickname {nickname} is invalid", + data={'nickname': new_name} + ) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 05ace03865..afa5578267 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- import time -from sets import Set -from utils import distance -from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot import logger +from pokemongo_bot.human_behaviour import (normalized_reticle_size, sleep, + spin_modifier) +from pokemongo_bot.cell_workers.base_task import BaseTask -class PokemonCatchWorker(object): +class PokemonCatchWorker(BaseTask): BAG_FULL = 'bag_full' NO_POKEBALLS = 'no_pokeballs' @@ -19,288 +18,502 @@ def __init__(self, pokemon, bot): self.pokemon_list = bot.pokemon_list self.item_list = bot.item_list self.inventory = bot.inventory + self.spawn_point_guid = '' + self.response_key = '' + self.response_status_key = '' - def work(self): + def work(self, response_dict=None): encounter_id = self.pokemon['encounter_id'] - spawnpoint_id = self.pokemon['spawnpoint_id'] - player_latitude = self.pokemon['latitude'] - player_longitude = self.pokemon['longitude'] - self.api.encounter(encounter_id=encounter_id, spawnpoint_id=spawnpoint_id, - player_latitude=player_latitude, player_longitude=player_longitude) - response_dict = self.api.call() - if response_dict and 'responses' in response_dict: - if 'ENCOUNTER' in response_dict['responses']: - if 'status' in response_dict['responses']['ENCOUNTER']: - if response_dict['responses']['ENCOUNTER']['status'] is 7: - logger.log('[x] Pokemon Bag is full!', 'red') - return PokemonCatchWorker.BAG_FULL + if not response_dict: + response_dict = self.create_encounter_api_call() - if response_dict['responses']['ENCOUNTER']['status'] is 1: + if response_dict and 'responses' in response_dict: + if self.response_key in response_dict['responses']: + if self.response_status_key in response_dict['responses'][self.response_key]: + if response_dict['responses'][self.response_key][self.response_status_key] is 1: cp = 0 - total_IV = 0 - if 'wild_pokemon' in response_dict['responses']['ENCOUNTER']: - pokemon = response_dict['responses']['ENCOUNTER']['wild_pokemon'] - catch_rate = response_dict['responses']['ENCOUNTER']['capture_probability']['capture_probability'] # 0 = pokeballs, 1 great balls, 3 ultra balls + if 'wild_pokemon' in response_dict['responses'][self.response_key] or 'pokemon_data' in \ + response_dict['responses'][self.response_key]: + if self.response_key == 'ENCOUNTER': + pokemon = response_dict['responses'][self.response_key]['wild_pokemon'] + else: + pokemon = response_dict['responses'][self.response_key] + + catch_rate = response_dict['responses'][self.response_key]['capture_probability'][ + 'capture_probability'] # 0 = pokeballs, 1 great balls, 3 ultra balls if 'pokemon_data' in pokemon and 'cp' in pokemon['pokemon_data']: - cp = pokemon['pokemon_data']['cp'] - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - - for individual_stat in iv_stats: - try: - total_IV += pokemon['pokemon_data'][individual_stat] - except: - pokemon['pokemon_data'][individual_stat] = 0 - continue - - pokemon_potential = round((total_IV / 45.0), 2) - pokemon_num = int(pokemon['pokemon_data'][ - 'pokemon_id']) - 1 - pokemon_name = self.pokemon_list[ - int(pokemon_num)]['Name'] - logger.log('[#] A Wild {} appeared! [CP {}] [Potential {}]'.format( - pokemon_name, cp, pokemon_potential), 'yellow') - - logger.log('[#] IV [Stamina/Attack/Defense] = [{}/{}/{}]'.format( - pokemon['pokemon_data']['individual_stamina'], - pokemon['pokemon_data']['individual_attack'], - pokemon['pokemon_data']['individual_defense'] - )) - pokemon['pokemon_data']['name'] = pokemon_name + pokemon_data = pokemon['pokemon_data'] + cp = pokemon_data['cp'] + + individual_attack = pokemon_data.get("individual_attack", 0) + individual_stamina = pokemon_data.get("individual_stamina", 0) + individual_defense = pokemon_data.get("individual_defense", 0) + + iv_display = '{}/{}/{}'.format( + individual_attack, + individual_defense, + individual_stamina + ) + + pokemon_potential = self.pokemon_potential(pokemon_data) + pokemon_num = int(pokemon_data['pokemon_id']) - 1 + pokemon_name = self.pokemon_list[int(pokemon_num)]['Name'] + + msg = 'A wild {pokemon} appeared! [CP {cp}] [Potential {iv}] [S/A/D {iv_display}]' + self.emit_event( + 'pokemon_appeared', + formatted=msg, + data={ + 'pokemon': pokemon_name, + 'cp': cp, + 'iv': pokemon_potential, + 'iv_display': iv_display, + } + ) + + pokemon_data['name'] = pokemon_name # Simulate app sleep(3) - balls_stock = self.bot.pokeball_inventory() - while(True): - - pokeball = 1 # default:poke ball - - if balls_stock[1] <= 0: # if poke ball are out of stock - if balls_stock[2] > 0: # and player has great balls in stock... - pokeball = 2 # then use great balls - elif balls_stock[3] > 0: # or if great balls are out of stock too, and player has ultra balls... - pokeball = 3 # then use ultra balls - else: - pokeball = 0 # player doesn't have any of pokeballs, great balls or ultra balls + if not self.should_capture_pokemon(pokemon_name, cp, pokemon_potential, response_dict): + return False + + flag_VIP = False + # @TODO, use the best ball in stock to catch VIP (Very Important Pokemon: Configurable) + if self.check_vip_pokemon(pokemon_name, cp, pokemon_potential): + self.emit_event( + 'vip_pokemon', + formatted='This is a VIP pokemon. Catch!!!' + ) + flag_VIP=True + + items_stock = self.bot.current_inventory() + berry_id = 701 # @ TODO: use better berries if possible + berries_count = self.bot.item_inventory_count(berry_id) + while True: + # pick the most simple ball from stock + pokeball = 1 # start from 1 - PokeBalls + berry_used = False + + if flag_VIP: + if(berries_count>0 and catch_rate[pokeball-1] < 0.9): + success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) + self.emit_event( + 'pokemon_catch_rate', + level='debug', + formatted="Catch rate of {catch_rate} is low. Maybe will throw {berry_name} ({berry_count} left)", + data={ + 'catch_rate': success_percentage, + 'berry_name': self.item_list[str(berry_id)], + 'berry_count': berries_count + } + ) + # Out of all pokeballs! Let's don't waste berry. + if items_stock[1] == 0 and items_stock[2] == 0 and items_stock[3] == 0: + break + + # Use the berry to catch + 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']: + catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] + success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) + berries_count = berries_count -1 + berry_used = True + self.emit_event( + 'threw_berry', + formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", + data={ + "berry_name": self.item_list[str(berry_id)], + "new_catch_rate": success_percentage + } + ) + else: + if response_dict['status_code'] is 1: + self.emit_event( + 'softban', + level='warning', + formatted='Failed to use berry. You may be softbanned.' + ) + else: + self.emit_event( + 'threw_berry_failed', + formatted='Unknown response when throwing berry: {status_code}.', + data={ + 'status_code': response_dict['status_code'] + } + ) + + #use the best ball to catch + current_type = pokeball + #debug use normal ball + while current_type < 3: + current_type += 1 + if catch_rate[pokeball-1] < 0.9 and items_stock[current_type] > 0: + # if current ball chance to catch is under 90%, and player has better ball - then use it + pokeball = current_type # use better ball + else: + # If we have a lot of berries (than the great ball), we prefer use a berry first! + if catch_rate[pokeball-1] < 0.42 and items_stock[pokeball+1]+30 < berries_count: + # If it's not the VIP type, we don't want to waste our ultra ball if no balls left. + if items_stock[1] == 0 and items_stock[2] == 0: + break + + success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) + self.emit_event( + 'pokemon_catch_rate', + level='debug', + formatted="Catch rate of {catch_rate} is low. Maybe will throw {berry_name} ({berry_count} left)", + data={ + 'catch_rate': success_percentage, + 'berry_name': self.item_list[str(berry_id)], + 'berry_count': berries_count-1 + } + ) + 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']: + catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] + success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) + berries_count = berries_count -1 + berry_used = True + self.emit_event( + 'threw_berry', + formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", + data={ + "berry_name": self.item_list[str(berry_id)], + "new_catch_rate": success_percentage + } + ) + else: + if response_dict['status_code'] is 1: + self.emit_event( + 'softban', + level='warning', + formatted='Failed to use berry. You may be softbanned.' + ) + else: + self.emit_event( + 'threw_berry_failed', + formatted='Unknown response when throwing berry: {status_code}.', + data={ + 'status_code': response_dict['status_code'] + } + ) - while(pokeball < 3): - if catch_rate[pokeball-1] < 0.35 and balls_stock[pokeball+1] > 0: - # if current ball chance to catch is under 35%, and player has better ball - then use it - pokeball = pokeball+1 # use better ball else: - break - - # @TODO, use the best ball in stock to catch VIP (Very Important Pokemon: Configurable) + #We don't have many berry to waste, pick a good ball first. Save some berry for future VIP pokemon + current_type = pokeball + while current_type < 2: + current_type += 1 + if catch_rate[pokeball-1] < 0.35 and items_stock[current_type] > 0: + # if current ball chance to catch is under 35%, and player has better ball - then use it + pokeball = current_type # use better ball + + #if the rate is still low and we didn't throw a berry before use berry + if catch_rate[pokeball-1] < 0.35 and berries_count > 0 and berry_used == False: + # If it's not the VIP type, we don't want to waste our ultra ball if no balls left. + if items_stock[1] == 0 and items_stock[2] == 0: + break + + success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) + self.emit_event( + 'pokemon_catch_rate', + level='debug', + formatted="Catch rate of {catch_rate} is low. Throwing {berry_name} ({berry_count} left)", + data={ + 'catch_rate': success_percentage, + 'berry_name': self.item_list[str(berry_id)], + 'berry_count': berries_count-1 + } + ) + 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']: + catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] + success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) + berries_count = berries_count -1 + berry_used = True + self.emit_event( + 'threw_berry', + formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", + data={ + "berry_name": self.item_list[str(berry_id)], + "new_catch_rate": success_percentage + } + ) + else: + if response_dict['status_code'] is 1: + self.emit_event( + 'softban', + level='warning', + formatted='Failed to use berry. You may be softbanned.' + ) + else: + self.emit_event( + 'threw_berry_failed', + formatted='Unknown response when throwing berry: {status_code}.', + data={ + 'status_code': response_dict['status_code'] + } + ) + + # Re-check if berry is used, find a ball for a good capture rate + current_type=pokeball + while current_type < 2: + current_type += 1 + if catch_rate[pokeball-1] < 0.35 and items_stock[current_type] > 0: + pokeball = current_type # use better ball + + # This is to avoid rare case that a berry has ben throwed <0.42 + # and still picking normal pokeball (out of stock) -> error + if items_stock[1] == 0 and items_stock[2] > 0: + pokeball = 2 + + # Add this logic to avoid Pokeball = 0, Great Ball = 0, Ultra Ball = X + # And this logic saves Ultra Balls if it's a weak trash pokemon + if catch_rate[pokeball-1]<0.30 and items_stock[3]>0: + pokeball = 3 + + items_stock[pokeball] -= 1 + success_percentage = '{0:.2f}'.format(catch_rate[pokeball - 1] * 100) + self.emit_event( + 'threw_pokeball', + formatted='Used {pokeball}, with chance {success_percentage} ({count_left} left)', + data={ + 'pokeball': self.item_list[str(pokeball)], + 'success_percentage': success_percentage, + 'count_left': items_stock[pokeball] + } + ) + id_list1 = self.count_pokemon_inventory() - if pokeball is 0: - logger.log( - '[x] Out of pokeballs, switching to farming mode...', 'red') - # Begin searching for pokestops. - self.config.mode = 'farm' - return PokemonCatchWorker.NO_POKEBALLS + reticle_size_parameter = normalized_reticle_size(self.config.catch_randomize_reticle_factor) + spin_modifier_parameter = spin_modifier(self.config.catch_randomize_spin_factor) - balls_stock[pokeball] = balls_stock[pokeball] - 1 - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - logger.log('[x] Using {} (chance: {}%)... ({} left!)'.format( - self.item_list[str(pokeball)], - success_percentage, - balls_stock[pokeball] - )) - - id_list1 = self.count_pokemon_inventory() - self.api.catch_pokemon(encounter_id=encounter_id, - pokeball=pokeball, - normalized_reticle_size=1.950, - spawn_point_guid=spawnpoint_id, - hit_pokemon=1, - spin_modifier=1, - NormalizedHitPosition=1) - response_dict = self.api.call() + response_dict = 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 + ) if response_dict and \ - 'responses' in response_dict and \ - 'CATCH_POKEMON' in response_dict['responses'] and \ - 'status' in response_dict['responses']['CATCH_POKEMON']: + 'responses' in response_dict and \ + 'CATCH_POKEMON' in response_dict['responses'] and \ + 'status' in response_dict['responses']['CATCH_POKEMON']: status = response_dict['responses'][ 'CATCH_POKEMON']['status'] if status is 2: - logger.log( - '[-] Attempted to capture {}- failed.. trying again!'.format(pokemon_name), 'red') + self.emit_event( + 'pokemon_fled', + formatted="{pokemon} fled.", + data={'pokemon': pokemon_name} + ) sleep(2) continue if status is 3: - logger.log( - '[x] Oh no! {} vanished! :('.format(pokemon_name), 'red') + self.emit_event( + 'pokemon_vanished', + formatted="{pokemon} vanished!", + data={'pokemon': pokemon_name} + ) + if success_percentage == 100: + self.softban = True if status is 1: - logger.log( - '[x] Captured {}! [CP {}] [IV {}]'.format( - pokemon_name, - cp, - pokemon_potential - ), 'green' + self.bot.metrics.captured_pokemon(pokemon_name, cp, iv_display, pokemon_potential) + + self.emit_event( + 'pokemon_caught', + formatted='Captured {pokemon}! [CP {cp}] [Potential {iv}] [{iv_display}] [+{exp} exp]', + data={ + 'pokemon': pokemon_name, + 'cp': cp, + 'iv': pokemon_potential, + 'iv_display': iv_display, + 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) + } ) + self.bot.softban = False - id_list2 = self.count_pokemon_inventory() + if (self.config.evolve_captured + and (self.config.evolve_captured[0] == 'all' + or pokemon_name in self.config.evolve_captured)): + id_list2 = self.count_pokemon_inventory() + # No need to capture this even for metrics, player stats includes it. + pokemon_to_transfer = list(set(id_list2) - set(id_list1)) - if self.config.evolve_captured: - pokemon_to_transfer = list(Set(id_list2) - Set(id_list1)) - self.api.evolve_pokemon(pokemon_id=pokemon_to_transfer[0]) - response_dict = self.api.call() + # TODO dont throw RuntimeError, do something better + if len(pokemon_to_transfer) == 0: + raise RuntimeError( + 'Trying to evolve 0 pokemons!') + response_dict = self.api.evolve_pokemon(pokemon_id=pokemon_to_transfer[0]) status = response_dict['responses']['EVOLVE_POKEMON']['result'] if status == 1: - logger.log( - '[#] {} has been evolved!'.format(pokemon_name), 'green') + self.emit_event( + 'pokemon_evolved', + formatted="{pokemon} evolved!", + data={'pokemon': pokemon_name} + ) else: - logger.log( - '[x] Failed to evolve {}!'.format(pokemon_name)) - - if self.should_release_pokemon(pokemon_name, cp, pokemon_potential, response_dict): - # Transfering Pokemon - pokemon_to_transfer = list( - Set(id_list2) - Set(id_list1)) - if len(pokemon_to_transfer) == 0: - raise RuntimeError( - 'Trying to transfer 0 pokemons!') - self.transfer_pokemon( - pokemon_to_transfer[0]) - logger.log( - '[#] {} has been exchanged for candy!'.format(pokemon_name), 'green') - else: - logger.log( - '[x] Captured {}! [CP {}]'.format(pokemon_name, cp), 'green') + self.emit_event( + 'pokemon_evolve_fail', + formatted="Failed to evolve {pokemon}!", + data={'pokemon': pokemon_name} + ) break time.sleep(5) - def _transfer_low_cp_pokemon(self, value): - self.api.get_inventory() - response_dict = self.api.call() - self._transfer_all_low_cp_pokemon(value, response_dict) + def count_pokemon_inventory(self): + # don't use cached bot.get_inventory() here + # because we need to have actual information in capture logic + response_dict = self.api.get_inventory() + + id_list = [] + callback = lambda pokemon: id_list.append(pokemon['id']) + self._foreach_pokemon_in_inventory(response_dict, callback) + return id_list - def _transfer_all_low_cp_pokemon(self, value, response_dict): + def _foreach_pokemon_in_inventory(self, response_dict, callback): try: reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) + "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) except KeyError: pass else: for item in response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']: try: reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon"], item) + "inventory_item_data", "pokemon_data"], item) except KeyError: pass else: - pokemon = item['inventory_item_data']['pokemon'] - self._execute_pokemon_transfer(value, pokemon) - time.sleep(1.2) + pokemon = item['inventory_item_data']['pokemon_data'] + if not pokemon.get('is_egg', False): + callback(pokemon) + + def pokemon_potential(self, pokemon_data): + total_iv = 0 + iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] + + for individual_stat in iv_stats: + try: + total_iv += pokemon_data[individual_stat] + except: + pokemon_data[individual_stat] = 0 + continue + + return round((total_iv / 45.0), 2) + + def should_capture_pokemon(self, pokemon_name, cp, iv, response_dict): + catch_config = self._get_catch_config_for(pokemon_name) + cp_iv_logic = catch_config.get('logic') + if not cp_iv_logic: + cp_iv_logic = self._get_catch_config_for('any').get('logic', 'and') + + catch_results = { + 'cp': False, + 'iv': False, + } + + if catch_config.get('never_catch', False): + return False - def _execute_pokemon_transfer(self, value, pokemon): - if 'cp' in pokemon and pokemon['cp'] < value: - self.api.release_pokemon(pokemon_id=pokemon['id']) - response_dict = self.api.call() + if catch_config.get('always_catch', False): + return True - def transfer_pokemon(self, pid): - self.api.release_pokemon(pokemon_id=pid) - response_dict = self.api.call() + catch_cp = catch_config.get('catch_above_cp', 0) + if cp > catch_cp: + catch_results['cp'] = True - def count_pokemon_inventory(self): - self.api.get_inventory() - response_dict = self.api.call() - id_list = [] - return self.counting_pokemon(response_dict, id_list) + catch_iv = catch_config.get('catch_above_iv', 0) + if iv > catch_iv: + catch_results['iv'] = True - def counting_pokemon(self, response_dict, id_list): - try: - reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) - except KeyError: - pass - else: - for item in response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data"], item) - except KeyError: - pass - else: - pokemon = item['inventory_item_data']['pokemon_data'] - if pokemon.get('is_egg', False): - continue - id_list.append(pokemon['id']) + logic_to_function = { + 'or': lambda x, y: x or y, + 'and': lambda x, y: x and y + } - return id_list + return logic_to_function[cp_iv_logic](*catch_results.values()) - def should_release_pokemon(self, pokemon_name, cp, iv, response_dict): - if self._check_always_capture_exception_for(pokemon_name): - return False + def _get_catch_config_for(self, pokemon): + catch_config = self.config.catch.get(pokemon) + if not catch_config: + catch_config = self.config.catch.get('any') + return catch_config + + def create_encounter_api_call(self): + encounter_id = self.pokemon['encounter_id'] + 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' + request.encounter( + encounter_id=encounter_id, + spawn_point_id=spawn_point_id, + player_latitude=player_latitude, + player_longitude=player_longitude + ) else: - release_config = self._get_release_config_for(pokemon_name) - cp_iv_logic = release_config.get('cp_iv_logic') - if not cp_iv_logic: - cp_iv_logic = self._get_release_config_for('any').get('cp_iv_logic', 'and') - - release_results = { - 'cp': False, - 'iv': False, - } - - if 'release_under_cp' in release_config: - min_cp = release_config['release_under_cp'] - if cp < min_cp: - release_results['cp'] = True - - if 'release_under_iv' in release_config: - min_iv = release_config['release_under_iv'] - if iv < min_iv: - release_results['iv'] = True - - if release_config.get('always_release'): - return True - - logic_to_function = { - 'or': lambda x, y: x or y, - 'and': lambda x, y: x and y - } - - #logger.log( - # "[x] Release config for {}: CP {} {} IV {}".format( - # pokemon_name, - # min_cp, - # cp_iv_logic, - # min_iv - # ), 'yellow' - #) - - return logic_to_function[cp_iv_logic](*release_results.values()) - - def _get_release_config_for(self, pokemon): - release_config = self.config.release_config.get(pokemon) - if not release_config: - release_config = self.config.release_config['any'] - return release_config - - def _get_exceptions(self): - exceptions = self.config.release_config.get('exceptions') - if not exceptions: - return None - return exceptions - - def _get_always_capture_list(self): - exceptions = self._get_exceptions() - if not exceptions: - return [] - always_capture_list = exceptions['always_capture'] - if not always_capture_list: - return [] - return always_capture_list - - def _check_always_capture_exception_for(self, pokemon_name): - always_capture_list = self._get_always_capture_list() - if not always_capture_list: - return False + fort_id = self.pokemon['fort_id'] + self.spawn_point_guid = fort_id + self.response_key = 'DISK_ENCOUNTER' + self.response_status_key = 'result' + 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): + + vip_name = self.config.vips.get(pokemon) + if vip_name == {}: + return True else: - for pokemon in always_capture_list: - if pokemon_name == str(pokemon): - return True - return False + catch_config = self.config.vips.get("any") + if not catch_config: + return False + cp_iv_logic = catch_config.get('logic', 'or') + catch_results = { + 'cp': False, + 'iv': False, + } + + catch_cp = catch_config.get('catch_above_cp', 0) + if cp > catch_cp: + catch_results['cp'] = True + catch_iv = catch_config.get('catch_above_iv', 0) + if iv > catch_iv: + catch_results['iv'] = True + logic_to_function = { + 'or': lambda x, y: x or y, + 'and': lambda x, y: x and y + } + return logic_to_function[cp_iv_logic](*catch_results.values()) diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py new file mode 100644 index 0000000000..c28b2749b1 --- /dev/null +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -0,0 +1,63 @@ +import json +import os +from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.tree_config_builder import ConfigException + +class RecycleItems(BaseTask): + def initialize(self): + self.item_filter = self.config.get('item_filter', {}) + self._validate_item_filter() + + def _validate_item_filter(self): + item_list = json.load(open(os.path.join('data', 'items.json'))) + for config_item_name, bag_count in self.item_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 work(self): + 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 + ) diff --git a/pokemongo_bot/cell_workers/seen_fort_worker.py b/pokemongo_bot/cell_workers/seen_fort_worker.py deleted file mode 100644 index a1faafa66d..0000000000 --- a/pokemongo_bot/cell_workers/seen_fort_worker.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- coding: utf-8 -*- - -import json -import time -from math import radians, sqrt, sin, cos, atan2 -from pgoapi.utilities import f2i, h2f -from utils import print_green, print_yellow, print_red, format_time -from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot import logger - - -class SeenFortWorker(object): - def __init__(self, fort, bot): - self.fort = fort - self.api = bot.api - self.bot = bot - self.position = bot.position - self.config = bot.config - self.item_list = bot.item_list - self.rest_time = 50 - self.stepper = bot.stepper - - def work(self): - lat = self.fort['latitude'] - lng = self.fort['longitude'] - - self.api.fort_details(fort_id=self.fort['id'], - latitude=lat, - longitude=lng) - response_dict = self.api.call() - if 'responses' in response_dict \ - and'FORT_DETAILS' in response_dict['responses'] \ - and 'name' in response_dict['responses']['FORT_DETAILS']: - fort_details = response_dict['responses']['FORT_DETAILS'] - fort_name = fort_details['name'].encode('utf8', 'replace') - else: - fort_name = 'Unknown' - logger.log('[#] Now at Pokestop: ' + fort_name + ' - Spinning...', - 'yellow') - sleep(2) - self.api.fort_search(fort_id=self.fort['id'], - fort_latitude=lat, - fort_longitude=lng, - player_latitude=f2i(self.position[0]), - player_longitude=f2i(self.position[1])) - response_dict = self.api.call() - if 'responses' in response_dict and \ - 'FORT_SEARCH' in response_dict['responses']: - - spin_details = response_dict['responses']['FORT_SEARCH'] - if spin_details['result'] == 1: - logger.log("[+] Loot: ", 'green') - experience_awarded = spin_details.get('experience_awarded', - False) - if experience_awarded: - logger.log("[+] " + str(experience_awarded) + " xp", - 'green') - - items_awarded = spin_details.get('items_awarded', False) - if items_awarded: - tmp_count_items = {} - for item in items_awarded: - item_id = item['item_id'] - if not item_id in tmp_count_items: - tmp_count_items[item_id] = item['item_count'] - else: - tmp_count_items[item_id] += item['item_count'] - - for item_id, item_count in tmp_count_items.iteritems(): - item_name = self.item_list[str(item_id)] - - logger.log("[+] " + str(item_count) + - "x " + item_name + - " (Total: " + str(self.bot.item_inventory_count(item_id)) + ")", 'green') - - # RECYCLING UNWANTED ITEMS - if str(item_id) in self.config.item_filter: - logger.log("[+] Recycling " + str(item_count) + "x " + item_name + "...", 'green') - #RECYCLE_INVENTORY_ITEM - response_dict_recycle = self.bot.drop_item(item_id=item_id, count=item_count) - - if response_dict_recycle and \ - 'responses' in response_dict_recycle and \ - 'RECYCLE_INVENTORY_ITEM' in response_dict_recycle['responses'] and \ - 'result' in response_dict_recycle['responses']['RECYCLE_INVENTORY_ITEM']: - result = response_dict_recycle['responses']['RECYCLE_INVENTORY_ITEM']['result'] - if result is 1: # Request success - logger.log("[+] Recycling success", 'green') - else: - logger.log("[+] Recycling failed!", 'red') - else: - logger.log("[#] Nothing found.", 'yellow') - - pokestop_cooldown = spin_details.get( - 'cooldown_complete_timestamp_ms') - if pokestop_cooldown: - seconds_since_epoch = time.time() - logger.log('[#] PokeStop on cooldown. Time left: ' + str( - format_time((pokestop_cooldown / 1000) - - seconds_since_epoch))) - - if not items_awarded and not experience_awarded and not pokestop_cooldown: - message = ( - 'Stopped at Pokestop and did not find experience, items ' - 'or information about the stop cooldown. You are ' - 'probably softbanned. Try to play on your phone, ' - 'if pokemons always ran away and you find nothing in ' - 'PokeStops you are indeed softbanned. Please try again ' - 'in a few hours.') - raise RuntimeError(message) - elif spin_details['result'] == 2: - logger.log("[#] Pokestop out of range") - elif spin_details['result'] == 3: - pokestop_cooldown = spin_details.get( - 'cooldown_complete_timestamp_ms') - if pokestop_cooldown: - seconds_since_epoch = time.time() - logger.log('[#] PokeStop on cooldown. Time left: ' + str( - format_time((pokestop_cooldown / 1000) - - seconds_since_epoch))) - elif spin_details['result'] == 4: - print_red("[#] Inventory is full, switching to catch mode...") - self.config.mode = 'poke' - - if 'chain_hack_sequence_number' in response_dict['responses'][ - 'FORT_SEARCH']: - time.sleep(2) - return response_dict['responses']['FORT_SEARCH'][ - 'chain_hack_sequence_number'] - else: - print_yellow('[#] may search too often, lets have a rest') - return 11 - sleep(8) - return 0 - - @staticmethod - def closest_fort(current_lat, current_long, forts): - print x diff --git a/pokemongo_bot/cell_workers/sleep_schedule.py b/pokemongo_bot/cell_workers/sleep_schedule.py new file mode 100644 index 0000000000..daaf0b8f1e --- /dev/null +++ b/pokemongo_bot/cell_workers/sleep_schedule.py @@ -0,0 +1,108 @@ +from datetime import datetime, timedelta +from time import sleep +from random import uniform +from pokemongo_bot.cell_workers.base_task import BaseTask + + +class SleepSchedule(BaseTask): + """Pauses the execution of the bot every day for some time + + Simulates the user going to sleep every day for some time, the sleep time + and the duration is changed every day by a random offset defined in the + config file + Example Config: + { + "type": "SleepSchedule", + "config": { + "time": "12:00", + "duration":"5:30", + "time_random_offset": "00:30", + "duration_random_offset": "00:30" + } + } + time: (HH:MM) local time that the bot should sleep + duration: (HH:MM) the duration of sleep + time_random_offset: (HH:MM) random offset of time that the sleep will start + for this example the possible start time is 11:30-12:30 + duration_random_offset: (HH:MM) random offset of duration of sleep + for this example the possible duration is 5:00-6:00 + """ + + LOG_INTERVAL_SECONDS = 600 + SCHEDULING_MARGIN = timedelta(minutes=10) # Skip if next sleep is RESCHEDULING_MARGIN from now + + def initialize(self): + # self.bot.event_manager.register_event('sleeper_scheduled', parameters=('datetime',)) + self._process_config() + self._schedule_next_sleep() + + def work(self): + if datetime.now() >= self._next_sleep: + self._sleep() + self._schedule_next_sleep() + self.bot.login() + + def _process_config(self): + self.time = datetime.strptime(self.config.get('time', '01:00'), '%H:%M') + + # Using datetime for easier stripping of timedeltas + duration = datetime.strptime(self.config.get('duration', '07:00'), '%H:%M') + self.duration = int(timedelta(hours=duration.hour, minutes=duration.minute).total_seconds()) + + time_random_offset = datetime.strptime(self.config.get('time_random_offset', '01:00'), '%H:%M') + self.time_random_offset = int( + timedelta( + hours=time_random_offset.hour, minutes=time_random_offset.minute).total_seconds()) + + duration_random_offset = datetime.strptime(self.config.get('duration_random_offset', '00:30'), '%H:%M') + self.duration_random_offset = int( + timedelta( + hours=duration_random_offset.hour, minutes=duration_random_offset.minute).total_seconds()) + + def _schedule_next_sleep(self): + self._next_sleep = self._get_next_sleep_schedule() + self._next_duration = self._get_next_duration() + self.emit_event( + 'next_sleep', + formatted="Next sleep at {time}", + data={ + 'time': str(self._next_sleep) + } + ) + + def _get_next_sleep_schedule(self): + now = datetime.now() + self.SCHEDULING_MARGIN + next_time = now.replace(hour=self.time.hour, minute=self.time.minute) + + next_time += timedelta(seconds=self._get_random_offset(self.time_random_offset)) + + # If sleep time is passed add one day + if next_time <= now: + next_time += timedelta(days=1) + + return next_time + + def _get_next_duration(self): + duration = self.duration + self._get_random_offset(self.duration_random_offset) + return duration + + def _get_random_offset(self, max_offset): + offset = uniform(-max_offset, max_offset) + return int(offset) + + def _sleep(self): + sleep_to_go = self._next_duration + self.emit_event( + 'bot_sleep', + formatted="Sleeping for {time_in_seconds}", + data={ + 'time_in_seconds': sleep_to_go + } + ) + while sleep_to_go > 0: + if sleep_to_go < self.LOG_INTERVAL_SECONDS: + sleep(sleep_to_go) + sleep_to_go = 0 + else: + sleep(self.LOG_INTERVAL_SECONDS) + sleep_to_go -= self.LOG_INTERVAL_SECONDS diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py new file mode 100644 index 0000000000..9572008241 --- /dev/null +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import time + +from pgoapi.utilities import f2i + +from pokemongo_bot.constants import Constants +from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.cell_workers.base_task import BaseTask +from utils import distance, format_time, fort_details + + +class SpinFort(BaseTask): + def should_run(self): + if not self.bot.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." + ) + return False + return True + + def work(self): + fort = self.get_fort_in_range() + + if not self.should_run() or fort is None: + return WorkerResult.SUCCESS + + lat = fort['latitude'] + lng = fort['longitude'] + + details = fort_details(self.bot, fort['id'], lat, lng) + fort_name = details.get('name', 'Unknown') + + 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']: + + spin_details = response_dict['responses']['FORT_SEARCH'] + spin_result = spin_details.get('result', -1) + if spin_result == 1: + self.bot.softban = False + experience_awarded = spin_details.get('experience_awarded', 0) + items_awarded = spin_details.get('items_awarded', {}) + if items_awarded: + self.bot.latest_inventory = None + tmp_count_items = {} + for item in items_awarded: + item_id = item['item_id'] + item_name = self.bot.item_list[str(item_id)] + if not item_name in tmp_count_items: + tmp_count_items[item_name] = item['item_count'] + else: + tmp_count_items[item_name] += item['item_count'] + + if experience_awarded or items_awarded: + self.emit_event( + 'spun_pokestop', + formatted="Spun pokestop {pokestop}. Experience awarded: {exp}. Items awarded: {items}", + data={ + 'pokestop': fort_name, + 'exp': experience_awarded, + 'items': tmp_count_items + } + ) + else: + self.emit_event( + 'pokestop_empty', + formatted='Found nothing in pokestop {pokestop}.', + data={'pokestop': fort_name} + ) + pokestop_cooldown = spin_details.get( + 'cooldown_complete_timestamp_ms') + self.bot.fort_timeouts.update({fort["id"]: pokestop_cooldown}) + self.bot.recent_forts = self.bot.recent_forts[1:] + [fort['id']] + elif spin_result == 2: + self.emit_event( + 'pokestop_out_of_range', + formatted="Pokestop {pokestop} out of range.", + data={'pokestop': fort_name} + ) + elif spin_result == 3: + pokestop_cooldown = spin_details.get( + 'cooldown_complete_timestamp_ms') + if pokestop_cooldown: + self.bot.fort_timeouts.update({fort["id"]: pokestop_cooldown}) + seconds_since_epoch = time.time() + minutes_left = format_time( + (pokestop_cooldown / 1000) - seconds_since_epoch + ) + self.emit_event( + 'pokestop_on_cooldown', + formatted="Pokestop {pokestop} on cooldown. Time left: {minutes_left}.", + data={'pokestop': fort_name, 'minutes_left': minutes_left} + ) + elif spin_result == 4: + self.emit_event( + 'inventory_full', + formatted="Inventory is full!" + ) + else: + self.emit_event( + 'unknown_spin_result', + formatted="Unknown spint result {status_code}", + data={'status_code': str(spin_result)} + ) + if 'chain_hack_sequence_number' in response_dict['responses'][ + 'FORT_SEARCH']: + time.sleep(2) + return response_dict['responses']['FORT_SEARCH'][ + 'chain_hack_sequence_number'] + else: + self.emit_event( + 'pokestop_searching_too_often', + formatted="Possibly searching too often, take a rest." + ) + if spin_result == 1 and not items_awarded and not experience_awarded and not pokestop_cooldown: + self.bot.softban = True + self.emit_event( + 'softban', + formatted='Probably got softban.' + ) + else: + self.bot.fort_timeouts[fort["id"]] = (time.time() + 300) * 1000 # Don't spin for 5m + return 11 + sleep(2) + return 0 + + def get_fort_in_range(self): + forts = self.bot.get_forts(order_by_distance=True) + + forts = filter(lambda x: x["id"] not in self.bot.fort_timeouts, forts) + + if len(forts) == 0: + return None + + fort = forts[0] + + distance_to_fort = distance( + self.bot.position[0], + self.bot.position[1], + fort['latitude'], + fort['longitude'] + ) + + if distance_to_fort <= Constants.MAX_DISTANCE_FORT_IS_REACHABLE: + return fort + + return None diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py new file mode 100644 index 0000000000..70c5939c58 --- /dev/null +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -0,0 +1,233 @@ +import json + +from pokemongo_bot.human_behaviour import action_delay +from pokemongo_bot.cell_workers.base_task import BaseTask + + +class TransferPokemon(BaseTask): + def work(self): + pokemon_groups = self._release_pokemon_get_groups() + for pokemon_id in pokemon_groups: + group = pokemon_groups[pokemon_id] + + if len(group) > 0: + pokemon_name = self.bot.pokemon_list[pokemon_id - 1]['Name'] + keep_best, keep_best_cp, keep_best_iv = self._validate_keep_best_config(pokemon_name) + + if keep_best: + best_pokemon_ids = set() + order_criteria = 'none' + if keep_best_cp >= 1: + cp_limit = keep_best_cp + best_cp_pokemons = sorted(group, key=lambda x: (x['cp'], x['iv']), reverse=True)[:cp_limit] + best_pokemon_ids = set(pokemon['pokemon_data']['id'] for pokemon in best_cp_pokemons) + order_criteria = 'cp' + + if keep_best_iv >= 1: + iv_limit = keep_best_iv + best_iv_pokemons = sorted(group, key=lambda x: (x['iv'], x['cp']), reverse=True)[:iv_limit] + best_pokemon_ids |= set(pokemon['pokemon_data']['id'] for pokemon in best_iv_pokemons) + if order_criteria == 'cp': + order_criteria = 'cp and iv' + else: + order_criteria = 'iv' + + # remove best pokemons from all pokemons array + all_pokemons = group + best_pokemons = [] + for best_pokemon_id in best_pokemon_ids: + for pokemon in all_pokemons: + if best_pokemon_id == pokemon['pokemon_data']['id']: + all_pokemons.remove(pokemon) + best_pokemons.append(pokemon) + + if best_pokemons and all_pokemons: + self.emit_event( + 'keep_best_release', + formatted="Keeping best {amount} {pokemon}, based on {criteria}", + data={ + 'amount': len(best_pokemons), + 'pokemon': pokemon_name, + 'criteria': order_criteria + } + ) + + transfer_pokemons = [pokemon for pokemon in all_pokemons + if self.should_release_pokemon(pokemon_name, + pokemon['cp'], + pokemon['iv'], + True)] + + if transfer_pokemons: + for pokemon in transfer_pokemons: + self.release_pokemon(pokemon_name, pokemon['cp'], pokemon['iv'], pokemon['pokemon_data']['id']) + else: + group = sorted(group, key=lambda x: x['cp'], reverse=True) + for item in group: + pokemon_cp = item['cp'] + pokemon_potential = item['iv'] + + if self.should_release_pokemon(pokemon_name, pokemon_cp, pokemon_potential): + self.release_pokemon(pokemon_name, item['cp'], item['iv'], item['pokemon_data']['id']) + + def _release_pokemon_get_groups(self): + pokemon_groups = {} + 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 + + inventory_dict = inventory_req['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] + + user_web_inventory = 'web/inventory-%s.json' % (self.bot.config.username) + with open(user_web_inventory, 'w') as outfile: + json.dump(inventory_dict, outfile) + + for pokemon in inventory_dict: + try: + reduce(dict.__getitem__, [ + "inventory_item_data", "pokemon_data", "pokemon_id" + ], pokemon) + except KeyError: + continue + + pokemon_data = pokemon['inventory_item_data']['pokemon_data'] + + # pokemon in fort, so we cant transfer it + if 'deployed_fort_id' in pokemon_data and pokemon_data['deployed_fort_id']: + continue + + # favorite pokemon can't transfer in official game client + if pokemon_data.get('favorite', 0) is 1: + continue + + group_id = pokemon_data['pokemon_id'] + group_pokemon_cp = pokemon_data['cp'] + group_pokemon_iv = self.get_pokemon_potential(pokemon_data) + + if group_id not in pokemon_groups: + pokemon_groups[group_id] = [] + + pokemon_groups[group_id].append({ + 'cp': group_pokemon_cp, + 'iv': group_pokemon_iv, + 'pokemon_data': pokemon_data + }) + + return pokemon_groups + + def get_pokemon_potential(self, pokemon_data): + total_iv = 0 + iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] + for individual_stat in iv_stats: + try: + total_iv += pokemon_data[individual_stat] + except Exception: + continue + return round((total_iv / 45.0), 2) + + def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): + release_config = self._get_release_config_for(pokemon_name) + + if (keep_best_mode + and not release_config.has_key('never_release') + and not release_config.has_key('always_release') + and not release_config.has_key('release_below_cp') + and not release_config.has_key('release_below_iv')): + return True + + cp_iv_logic = release_config.get('logic') + if not cp_iv_logic: + cp_iv_logic = self._get_release_config_for('any').get('logic', 'and') + + release_results = { + 'cp': False, + 'iv': False, + } + + if release_config.get('never_release', False): + return False + + if release_config.get('always_release', False): + return True + + release_cp = release_config.get('release_below_cp', 0) + if cp < release_cp: + release_results['cp'] = True + + release_iv = release_config.get('release_below_iv', 0) + if iv < release_iv: + release_results['iv'] = True + + logic_to_function = { + 'or': lambda x, y: x or y, + 'and': lambda x, y: x and y + } + + if logic_to_function[cp_iv_logic](*release_results.values()): + self.emit_event( + 'future_pokemon_release', + formatted="Releasing {pokemon} (CP {cp}/IV {iv}) based on rule: CP < {below_cp} {cp_iv_logic} IV < {below_iv}", + data={ + 'pokemon': pokemon_name, + 'cp': cp, + 'iv': iv, + 'below_cp': release_cp, + 'cp_iv_logic': cp_iv_logic.upper(), + 'below_iv': release_iv + } + ) + + return logic_to_function[cp_iv_logic](*release_results.values()) + + def release_pokemon(self, pokemon_name, cp, iv, pokemon_id): + response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon_id) + self.emit_event( + 'pokemon_release', + formatted='Exchanged {pokemon} [CP {cp}] [IV {iv}] for candy.', + data={ + 'pokemon': pokemon_name, + 'cp': cp, + 'iv': iv + } + ) + action_delay(self.bot.config.action_wait_min, self.bot.config.action_wait_max) + + def _get_release_config_for(self, pokemon): + release_config = self.bot.config.release.get(pokemon) + if not release_config: + release_config = self.bot.config.release.get('any') + if not release_config: + release_config = {} + return release_config + + def _validate_keep_best_config(self, pokemon_name): + keep_best = False + + release_config = self._get_release_config_for(pokemon_name) + + keep_best_cp = release_config.get('keep_best_cp', 0) + keep_best_iv = release_config.get('keep_best_iv', 0) + + if keep_best_cp or keep_best_iv: + keep_best = True + try: + keep_best_cp = int(keep_best_cp) + except ValueError: + keep_best_cp = 0 + + try: + keep_best_iv = int(keep_best_iv) + except ValueError: + keep_best_iv = 0 + + if keep_best_cp < 0 or keep_best_iv < 0: + keep_best = False + + if keep_best_cp == 0 and keep_best_iv == 0: + keep_best = False + + return keep_best, keep_best_cp, keep_best_iv diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py new file mode 100644 index 0000000000..911d2efd4d --- /dev/null +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -0,0 +1,233 @@ +import ctypes +from sys import stdout, platform as _platform +from datetime import datetime, timedelta + +from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.tree_config_builder import ConfigException + +class UpdateTitleStats(BaseTask): + """ + Periodically updates the terminal title to display stats about the bot. + + Fetching some stats requires making API calls. If you're concerned about the amount of calls + your bot is making, don't enable this worker. + + Example config : + { + "type": "UpdateTitleStats", + "config": { + "min_interval": 10, + "stats": ["uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"] + } + } + + Available stats : + - uptime : The bot uptime. + - km_walked : The kilometers walked since the bot started. + - level : The current character's level. + - level_completion : The current level experience, the next level experience and the completion + percentage. + - level_stats : Puts together the current character's level and its completion. + - xp_per_hour : The estimated gain of experience per hour. + - xp_earned : The experience earned since the bot started. + - stops_visited : The number of visited stops. + - pokemon_encountered : The number of encountered pokemon. + - pokemon_caught : The number of caught pokemon. + - pokemon_released : The number of released pokemon. + - pokemon_evolved : The number of evolved pokemon. + - pokemon_unseen : The number of pokemon never seen before. + - pokemon_stats : Puts together the pokemon encountered, caught, released, evolved and unseen. + - pokeballs_thrown : The number of thrown pokeballs. + - stardust_earned : The number of earned stardust since the bot started. + - highest_cp_pokemon : The caught pokemon with the highest CP since the bot started. + - most_perfect_pokemon : The most perfect caught pokemon since the bot started. + + min_interval : The minimum interval at which the title is updated, + in seconds (defaults to 10 seconds). + The update interval cannot be accurate as workers run synchronously. + stats : An array of stats to display and their display order (implicitly), + see available stats above. + """ + + DEFAULT_MIN_INTERVAL = 10 + DEFAULT_DISPLAYED_STATS = [] + + def __init__(self, bot, config): + """ + Initializes the worker. + :param bot: The bot instance. + :type bot: PokemonGoBot + :param config: The task configuration. + :type config: dict + """ + super(UpdateTitleStats, self).__init__(bot, config) + + self.next_update = None + self.min_interval = self.DEFAULT_MIN_INTERVAL + self.displayed_stats = self.DEFAULT_DISPLAYED_STATS + + self._process_config() + + def initialize(self): + pass + + def work(self): + """ + Updates the title if necessary. + :return: Always returns WorkerResult.SUCCESS. + :rtype: WorkerResult + """ + if not self._should_display(): + return WorkerResult.SUCCESS + title = self._get_stats_title(self._get_player_stats()) + # If title is empty, it couldn't be generated. + if not title: + return WorkerResult.SUCCESS + self._update_title(title, _platform) + return WorkerResult.SUCCESS + + def _should_display(self): + """ + Returns a value indicating whether the title should be updated. + :return: True if the title should be updated; otherwise, False. + :rtype: bool + """ + return self.next_update is None or datetime.now() >= self.next_update + + def _update_title(self, title, platform): + """ + Updates the window title using different methods, according to the given platform + :param title: The new window title. + :type title: string + :param platform: The platform string. + :type platform: string + :return: Nothing. + :rtype: None + :raise: RuntimeError: When the given platform isn't supported. + """ + if platform == "linux" or platform == "linux2"\ + or platform == "cygwin": + stdout.write("\x1b]2;{}\x07".format(title)) + elif platform == "darwin": + stdout.write("\033]0;{}\007".format(title)) + elif platform == "win32": + ctypes.windll.kernel32.SetConsoleTitleA(title) + else: + raise RuntimeError("unsupported platform '{}'".format(platform)) + + self.next_update = datetime.now() + timedelta(seconds=self.min_interval) + + def _process_config(self): + """ + Fetches the configuration for this worker and stores the values internally. + :return: Nothing. + :rtype: None + """ + self.min_interval = int(self.config.get('min_interval', self.DEFAULT_MIN_INTERVAL)) + self.displayed_stats = self.config.get('stats', self.DEFAULT_DISPLAYED_STATS) + + def _get_stats_title(self, player_stats): + """ + Generates a stats string with the given player stats according to the configuration. + :return: A string containing human-readable stats, ready to be displayed. + :rtype: string + """ + # No player stats available, won't be able to gather all informations. + if player_stats is None: + return '' + # No stats to display, avoid any useless overhead. + if not self.displayed_stats: + return '' + + # Gather stats values. + metrics = self.bot.metrics + metrics.capture_stats() + runtime = metrics.runtime() + distance_travelled = metrics.distance_travelled() + current_level = int(player_stats.get('level', 0)) + prev_level_xp = int(player_stats.get('prev_level_xp', 0)) + next_level_xp = int(player_stats.get('next_level_xp', 0)) + experience = int(player_stats.get('experience', 0)) + current_level_xp = experience - prev_level_xp + whole_level_xp = next_level_xp - prev_level_xp + level_completion_percentage = int((current_level_xp * 100) / whole_level_xp) + experience_per_hour = int(metrics.xp_per_hour()) + xp_earned = metrics.xp_earned() + stops_visited = metrics.visits['latest'] - metrics.visits['start'] + pokemon_encountered = metrics.num_encounters() + pokemon_caught = metrics.num_captures() + pokemon_released = metrics.releases + pokemon_evolved = metrics.num_evolutions() + pokemon_unseen = metrics.num_new_mons() + pokeballs_thrown = metrics.num_throws() + stardust_earned = metrics.earned_dust() + highest_cp_pokemon = metrics.highest_cp['desc'] + if not highest_cp_pokemon: + highest_cp_pokemon = "None" + most_perfect_pokemon = metrics.most_perfect['desc'] + if not most_perfect_pokemon: + most_perfect_pokemon = "None" + + # Create stats strings. + available_stats = { + 'uptime': 'Uptime : {}'.format(runtime), + 'km_walked': '{:,.2f}km walked'.format(distance_travelled), + 'level': 'Level {}'.format(current_level), + 'level_completion': '{:,} / {:,} XP ({}%)'.format(current_level_xp, whole_level_xp, + level_completion_percentage), + 'level_stats': 'Level {} ({:,} / {:,}, {}%)'.format(current_level, current_level_xp, + whole_level_xp, + level_completion_percentage), + 'xp_per_hour': '{:,} XP/h'.format(experience_per_hour), + 'xp_earned': '+{:,} XP'.format(xp_earned), + 'stops_visited': 'Visited {:,} stops'.format(stops_visited), + 'pokemon_encountered': 'Encountered {:,} pokemon'.format(pokemon_encountered), + 'pokemon_caught': 'Caught {:,} pokemon'.format(pokemon_caught), + 'pokemon_released': 'Released {:,} pokemon'.format(pokemon_released), + 'pokemon_evolved': 'Evolved {:,} pokemon'.format(pokemon_evolved), + 'pokemon_unseen': 'Encountered {} new pokemon'.format(pokemon_unseen), + 'pokemon_stats': 'Encountered {:,} pokemon, {:,} caught, {:,} released, {:,} evolved, ' + '{} never seen before'.format(pokemon_encountered, pokemon_caught, + pokemon_released, pokemon_evolved, + pokemon_unseen), + 'pokeballs_thrown': 'Threw {:,} pokeballs'.format(pokeballs_thrown), + 'stardust_earned': 'Earned {:,} Stardust'.format(stardust_earned), + 'highest_cp_pokemon': 'Highest CP pokemon : {}'.format(highest_cp_pokemon), + 'most_perfect_pokemon': 'Most perfect pokemon : {}'.format(most_perfect_pokemon), + } + + def get_stat(stat): + """ + Fetches a stat string from the available stats dictionary. + :param stat: The stat name. + :type stat: string + :return: The generated stat string. + :rtype: string + :raise: ConfigException: When the provided stat string isn't in the available stats + dictionary. + """ + if stat not in available_stats: + raise ConfigException("stat '{}' isn't available for displaying".format(stat)) + return available_stats[stat] + + # Map stats the user wants to see to available stats and join them with pipes. + title = ' | '.join(map(get_stat, self.displayed_stats)) + + return title + + def _get_player_stats(self): + """ + Helper method parsing the bot inventory object and returning the player stats object. + :return: The player stats object. + :rtype: dict + """ + inventory_items = self.bot.get_inventory() \ + .get('responses', {}) \ + .get('GET_INVENTORY', {}) \ + .get('inventory_delta', {}) \ + .get('inventory_items', {}) + return next((x["inventory_item_data"]["player_stats"] + for x in inventory_items + if x.get("inventory_item_data", {}).get("player_stats", {})), + None) diff --git a/pokemongo_bot/cell_workers/utils.py b/pokemongo_bot/cell_workers/utils.py index bd31375ccc..946c757b16 100644 --- a/pokemongo_bot/cell_workers/utils.py +++ b/pokemongo_bot/cell_workers/utils.py @@ -1,10 +1,49 @@ # -*- coding: utf-8 -*- import struct -from math import cos, asin, sqrt +from math import asin, atan, cos, exp, log, pi, sin, sqrt, tan + from colorama import init +from networkx.algorithms.clique import find_cliques + +import networkx as nx +import numpy as np + init() +TIME_PERIODS = ( + (60, 'minute'), + (3600, 'hour'), + (86400, 'day'), + (86400*7, 'week') +) + +FORT_CACHE = {} +def fort_details(bot, fort_id, latitude, longitude): + """ + Lookup fort metadata and (if possible) serve from cache. + """ + + if fort_id not in FORT_CACHE: + """ + Lookup the fort details and cache the response for future use. + """ + request = bot.api.create_request() + request.fort_details(fort_id=fort_id, latitude=latitude, longitude=longitude) + try: + response_dict = request.call() + FORT_CACHE[fort_id] = response_dict['responses']['FORT_DETAILS'] + except Exception: + pass + + # Just to avoid KeyErrors + return FORT_CACHE.get(fort_id, {}) + +def encode(cellid): + output = [] + encoder._VarintEncoder()(output.append, cellid) + return ''.join(output) + def distance(lat1, lon1, lat2, lon2): p = 0.017453292519943295 @@ -82,16 +121,17 @@ def format_dist(distance, unit): def format_time(seconds): # Return a string displaying the time given as seconds or minutes - if seconds <= 0.0: - return '{:.2f} seconds'.format(seconds) - elif seconds <= 1.0: - return '{:.2f} second'.format(seconds) - elif seconds < 60: - return '{:.2f} seconds'.format(seconds) - elif seconds > 60 and seconds < 3600: - minutes = seconds / 60 - return '{:.2f} minutes'.format(minutes) - return '{:.2f} seconds'.format(seconds) + num, duration = 0, long(round(seconds)) + runtime = [] + for period, unit in TIME_PERIODS[::-1]: + num, duration = divmod(duration, period) + if num: + p = '{0}{1}'.format(unit, 's'*(num!=1)) + runtime.append('{0} {1}'.format(num, p)) + + runtime.append('{0} second{1}'.format(duration, 's'*(duration!=1))) + + return ', '.join(runtime) def i2f(int): @@ -108,3 +148,89 @@ def print_yellow(message): def print_red(message): print(u'\033[91m' + message.decode('utf-8') + '\033[0m') + + +def float_equal(f1, f2, epsilon=1e-8): + if f1 > f2: + return f1 - f2 < epsilon + if f2 > f1: + return f2 - f1 < epsilon + return True + + +# pseudo mercator projection +EARTH_RADIUS_MAJ = 6378137.0 +EARTH_RADIUS_MIN = 6356752.3142 +RATIO = (EARTH_RADIUS_MIN / EARTH_RADIUS_MAJ) +ECCENT = sqrt(1.0 - RATIO**2) +COM = 0.5 * ECCENT + + +def coord2merc(lat, lng): + return lng2x(lng), lat2y(lat) + + +def merc2coord(vec): + return y2lat(vec[1]), x2lng(vec[0]) + + +def y2lat(y): + ts = exp(-y / EARTH_RADIUS_MAJ) + phi = pi / 2.0 - 2 * atan(ts) + dphi = 1.0 + for i in range(15): + if abs(dphi) < 0.000000001: + break + con = ECCENT * sin(phi) + dphi = pi / 2.0 - 2 * atan (ts * pow((1.0 - con) / (1.0 + con), COM)) - phi + phi += dphi + return rad2deg(phi) + + +def lat2y(lat): + lat = min(89.5, max(lat, -89.5)) + phi = deg2rad(lat) + sinphi = sin(phi) + con = ECCENT * sinphi + con = pow((1.0 - con) / (1.0 + con), COM) + ts = tan(0.5 * (pi * 0.5 - phi)) / con + return 0 - EARTH_RADIUS_MAJ * log(ts) + + +def x2lng(x): + return rad2deg(x) / EARTH_RADIUS_MAJ + + +def lng2x(lng): + return EARTH_RADIUS_MAJ * deg2rad(lng); + + +def deg2rad(deg): + return deg * pi / 180.0 + + +def rad2deg(rad): + return rad * 180.0 / pi + + +def find_biggest_cluster(radius, points, order=None): + graph = nx.Graph() + for point in points: + if order is 'lure_info': + f = point['latitude'], point['longitude'], point['lure_info']['lure_expires_timestamp_ms'] + else: + f = point['latitude'], point['longitude'], 0 + graph.add_node(f) + for node in graph.nodes(): + if node != f and distance(f[0], f[1], node[0], node[1]) <= radius*2: + graph.add_edge(f, node) + cliques = list(find_cliques(graph)) + if len(cliques) > 0: + max_clique = max(list(find_cliques(graph)), key=lambda l: (len(l), sum(x[2] for x in l))) + merc_clique = [coord2merc(x[0], x[1]) for x in max_clique] + clique_x, clique_y = zip(*merc_clique) + best_point = np.mean(clique_x), np.mean(clique_y) + best_coord = merc2coord(best_point) + return {'latitude': best_coord[0], 'longitude': best_coord[1], 'num_points': len(max_clique)} + else: + return None diff --git a/pokemongo_bot/constants.py b/pokemongo_bot/constants.py new file mode 100644 index 0000000000..b52aae45fb --- /dev/null +++ b/pokemongo_bot/constants.py @@ -0,0 +1,2 @@ +class Constants(object): + MAX_DISTANCE_FORT_IS_REACHABLE = 40 # meters diff --git a/pokemongo_bot/event_handlers/__init__.py b/pokemongo_bot/event_handlers/__init__.py new file mode 100644 index 0000000000..f0933a0e68 --- /dev/null +++ b/pokemongo_bot/event_handlers/__init__.py @@ -0,0 +1,2 @@ +from logging_handler import LoggingHandler +from socketio_handler import SocketIoHandler diff --git a/pokemongo_bot/event_handlers/logging_handler.py b/pokemongo_bot/event_handlers/logging_handler.py new file mode 100644 index 0000000000..7ad5720f6a --- /dev/null +++ b/pokemongo_bot/event_handlers/logging_handler.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +import logging + +from pokemongo_bot.event_manager import EventHandler + + +class LoggingHandler(EventHandler): + + def handle_event(self, event, sender, level, formatted_msg, data): + logger = logging.getLogger(type(sender).__name__) + if formatted_msg: + message = "[{}] {}".format(event, formatted_msg) + else: + message = '{}: {}'.format(event, str(data)) + getattr(logger, level)(message) diff --git a/pokemongo_bot/event_handlers/socketio_handler.py b/pokemongo_bot/event_handlers/socketio_handler.py new file mode 100644 index 0000000000..05b095313a --- /dev/null +++ b/pokemongo_bot/event_handlers/socketio_handler.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +from socketIO_client import SocketIO + +from pokemongo_bot.event_manager import EventHandler + + +class SocketIoHandler(EventHandler): + + + def __init__(self, bot, url): + self.bot = bot + self.host, port_str = url.split(':') + self.port = int(port_str) + self.sio = SocketIO(self.host, self.port) + + def handle_event(self, event, sender, level, msg, data): + if msg: + data['msg'] = msg + + self.sio.emit( + 'bot:broadcast', + { + 'event': event, + 'account': self.bot.config.username, + 'data': data + } + ) diff --git a/pokemongo_bot/event_manager.py b/pokemongo_bot/event_manager.py new file mode 100644 index 0000000000..3773ec8a9e --- /dev/null +++ b/pokemongo_bot/event_manager.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +class EventNotRegisteredException(Exception): + pass + + +class EventMalformedException(Exception): + pass + + +class EventHandler(object): + + def __init__(self): + pass + + def handle_event(self, event, kwargs): + raise NotImplementedError("Please implement") + + +class EventManager(object): + + def __init__(self, *handlers): + self._registered_events = dict() + self._handlers = handlers or [] + + def event_report(self): + for event, parameters in self._registered_events.iteritems(): + print '-'*80 + print 'Event: {}'.format(event) + if parameters: + print 'Parameters:' + for parameter in parameters: + print '* {}'.format(parameter) + + def add_handler(self, event_handler): + self._handlers.append(event_handler) + + def register_event(self, name, parameters=[]): + self._registered_events[name] = parameters + + def emit(self, event, sender=None, level='info', formatted='', data={}): + if not sender: + raise ArgumentError('Event needs a sender!') + + levels = ['info', 'warning', 'error', 'critical', 'debug'] + if not level in levels: + raise ArgumentError('Event level needs to be in: {}'.format(levels)) + + if event not in self._registered_events: + raise EventNotRegisteredException("Event %s not registered..." % event) + + # verify params match event + parameters = self._registered_events[event] + if parameters: + for k, v in data.iteritems(): + if k not in parameters: + raise EventMalformedException("Event %s does not require parameter %s" % (event, k)) + + formatted_msg = formatted.format(**data) + + # send off to the handlers + for handler in self._handlers: + handler.handle_event(event, sender, level, formatted_msg, data) diff --git a/pokemongo_bot/health_record/__init__.py b/pokemongo_bot/health_record/__init__.py new file mode 100644 index 0000000000..a40a959a1c --- /dev/null +++ b/pokemongo_bot/health_record/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from bot_event import BotEvent diff --git a/pokemongo_bot/health_record/bot_event.py b/pokemongo_bot/health_record/bot_event.py new file mode 100644 index 0000000000..986b5f3c70 --- /dev/null +++ b/pokemongo_bot/health_record/bot_event.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +from time import sleep + +import logging +from raven import Client +import raven +import os +import uuid +import requests + +class BotEvent(object): + def __init__(self, config): + self.config = config + self.logger = logging.getLogger(__name__) + # UniversalAnalytics can be reviewed here: + # https://github.com/analytics-pros/universal-analytics-python + if self.config.health_record: + self.logger.info('Health check is enabled. For more logrmation:') + self.logger.info('https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev#analytics') + self.client = Client( + dsn='https://8abac56480f34b998813d831de262514:196ae1d8dced41099f8253ea2c8fe8e6@app.getsentry.com/90254', + name='PokemonGof-Bot', + processors = ( + 'raven.processors.SanitizePasswordsProcessor', + 'raven.processors.RemoveStackLocalsProcessor' + ), + install_logging_hook = False, + hook_libraries = (), + enable_breadcrumbs = False, + logging = False, + context = {} + ) + + def capture_error(self): + if self.config.health_record: + self.client.captureException() + + def login_success(self): + if self.config.health_record: + track_url('/loggedin') + + def login_failed(self): + if self.config.health_record: + track_url('/login') + + def login_retry(self): + if self.config.health_record: + track_url('/relogin') + + def logout(self): + if self.config.health_record: + track_url('/logout') + + +def track_url(path): + data = { + 'v': '1', + 'tid': 'UA-81469507-1', + 'aip': '1', # Anonymize IPs + 'cid': uuid.uuid4(), + 't': 'pageview', + 'dp': path + } + + response = requests.post( + 'http://www.google-analytics.com/collect', data=data) + + response.raise_for_status() diff --git a/pokemongo_bot/human_behaviour.py b/pokemongo_bot/human_behaviour.py index 6d4c434f92..2a8d2d5e9f 100644 --- a/pokemongo_bot/human_behaviour.py +++ b/pokemongo_bot/human_behaviour.py @@ -1,17 +1,46 @@ # -*- coding: utf-8 -*- import time -from math import ceil -from random import random, randint +from random import random, uniform def sleep(seconds, delta=0.3): - jitter = ceil(delta * seconds) - sleep_time = randint(int(seconds - jitter), int(seconds + jitter)) - time.sleep(sleep_time) + time.sleep(jitter(seconds,delta)) + + +def jitter(value, delta=0.3): + jitter = delta * value + return uniform(value-jitter, value+jitter) + + +def action_delay(low, high): + # Waits for random number of seconds between low & high numbers + longNum = uniform(low, high) + shortNum = float("{0:.2f}".format(longNum)) + time.sleep(shortNum) 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/lcd.py b/pokemongo_bot/lcd.py index c4f4da4b40..9a4eb87e6f 100644 --- a/pokemongo_bot/lcd.py +++ b/pokemongo_bot/lcd.py @@ -9,8 +9,6 @@ # By DenisFromHR (Denis Pleic) # 2015-02-10, ver 0.1 """ -# -# import os from itertools import islice from time import * @@ -52,13 +50,13 @@ def read_data(self, cmd): def read_block_data(self, cmd): return self.bus.read_block_data(self.addr, cmd) + # LCD Address -#ADDRESS = 0x27 +# ADDRESS = 0x27 LCD_WIDTH = 20 LCD_HEIGHT = 2 -LCD_CHARS = [0x40, 0x48, 0x50, 0x58, 0x60, 0x68, 0x70, - 0x78] #Address position for custom chars -#Use char generator here: https://omerk.github.io/lcdchargen/ or http://www.quinapalus.com/hd44780udg.html +LCD_CHARS = [0x40, 0x48, 0x50, 0x58, 0x60, 0x68, 0x70, 0x78] # Address position for custom chars +# Use char generator here: https://omerk.github.io/lcdchargen/ or http://www.quinapalus.com/hd44780udg.html # commands LCD_CLEARDISPLAY = 0x01 LCD_RETURNHOME = 0x02 @@ -108,7 +106,7 @@ def read_block_data(self, cmd): class lcd: # initializes objects and lcd - #def __init__(self, adress): + # def __init__(self, adress): def set_addr(self, adress): self.lcd_device = i2c_device(adress) diff --git a/pokemongo_bot/logger.py b/pokemongo_bot/logger.py index 151e578a0f..ebb59b7599 100644 --- a/pokemongo_bot/logger.py +++ b/pokemongo_bot/logger.py @@ -1,22 +1,20 @@ -import time -try: - import lcd - lcd = lcd.lcd() - # Change this to your i2c address - lcd.set_addr(0x23) -except: - lcd = False +import warnings +import logging -def log(string, color = 'white'): - colorHex = { - 'green': '92m', - 'yellow': '93m', - 'red': '91m' - } - if color not in colorHex: - print('[' + time.strftime("%Y-%m-%d %H:%M:%S") + '] '+ string) - else: - print(u'\033['+ colorHex[color] + '[' + time.strftime("%Y-%m-%d %H:%M:%S") + '] ' + string.decode('utf-8') + '\033[0m') - if lcd: - if(string): - lcd.message(string) + +def log(msg, color=None): + warnings.simplefilter('always', DeprecationWarning) + message = ( + "Using logger.log is deprecated and will be removed soon. " + "We recommend that you try to log as little as possible " + "and use the event system to send important messages " + "(they become logs and websocket messages) automatically). " + "If you don't think your message should go to the websocket " + "server but it's really necessary, use the self.logger variable " + "inside any class inheriting from BaseTask to log." + + ) + + logger = logging.getLogger('generic') + logger.info(msg) + warnings.warn(message, DeprecationWarning) diff --git a/pokemongo_bot/metrics.py b/pokemongo_bot/metrics.py new file mode 100644 index 0000000000..0ffeb39a6c --- /dev/null +++ b/pokemongo_bot/metrics.py @@ -0,0 +1,113 @@ +import time +from datetime import timedelta + + +class Metrics(object): + + def __init__(self, bot): + self.bot = bot + self.start_time = time.time() + self.dust = {'start': None, 'latest': None} + self.xp = {'start': None, 'latest': None} + self.distance = {'start': None, 'latest': None} + self.encounters = {'start': None, 'latest': None} + self.throws = {'start': None, 'latest': None} + self.captures = {'start': None, 'latest': None} + self.visits = {'start': None, 'latest': None} + self.unique_mons = {'start': None, 'latest': None} + self.evolutions = {'start': None, 'latest': None} + + self.releases = 0 + self.highest_cp = {'cp': 0, 'desc': ''} + self.most_perfect = {'potential': 0, 'desc': ''} + + def runtime(self): + return timedelta(seconds=round(time.time() - self.start_time)) + + def xp_earned(self): + return self.xp['latest'] - self.xp['start'] + + def xp_per_hour(self): + return self.xp_earned()/(time.time() - self.start_time)*3600 + + def distance_travelled(self): + return self.distance['latest'] - self.distance['start'] + + def num_encounters(self): + return self.encounters['latest'] - self.encounters['start'] + + def num_throws(self): + return self.throws['latest'] - self.throws['start'] + + def num_captures(self): + return self.captures['latest'] - self.captures['start'] + + def num_visits(self): + return self.visits['latest'] - self.visits['start'] + + def num_new_mons(self): + return self.unique_mons['latest'] - self.unique_mons['start'] + + def num_evolutions(self): + return self.evolutions['latest'] - self.evolutions['start'] + + def earned_dust(self): + return self.dust['latest'] - self.dust['start'] + + def captured_pokemon(self, name, cp, iv_display, potential): + if cp > self.highest_cp['cp']: + self.highest_cp = \ + {'cp': cp, 'desc': '{} [CP: {}] [IV: {}] Potential: {} ' + .format(name, cp, iv_display, potential)} + + if potential > self.most_perfect['potential']: + self.most_perfect = \ + {'potential': potential, 'desc': '{} [CP: {}] [IV: {}] Potential: {} ' + .format(name, cp, iv_display, potential)} + return + + def released_pokemon(self, count=1): + self.releases += count + + def capture_stats(self): + 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'] + for item in response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']: + if 'inventory_item_data' in item: + if 'player_stats' in item['inventory_item_data']: + playerdata = item['inventory_item_data']['player_stats'] + + self.xp['latest'] = playerdata.get('experience', 0) + if self.xp['start'] is None: self.xp['start'] = self.xp['latest'] + + self.visits['latest'] = playerdata.get('poke_stop_visits', 0) + if self.visits['start'] is None: self.visits['start'] = self.visits['latest'] + + self.captures['latest'] = playerdata.get('pokemons_captured', 0) + if self.captures['start'] is None: self.captures['start'] = self.captures['latest'] + + self.distance['latest'] = playerdata.get('km_walked', 0) + if self.distance['start'] is None: self.distance['start'] = self.distance['latest'] + + self.encounters['latest'] = playerdata.get('pokemons_encountered', 0) + if self.encounters['start'] is None: self.encounters['start'] = self.encounters['latest'] + + self.throws['latest'] = playerdata.get('pokeballs_thrown', 0) + if self.throws['start'] is None: self.throws['start'] = self.throws['latest'] + + self.unique_mons['latest'] = playerdata.get('unique_pokedex_entries', 0) + if self.unique_mons['start'] is None: self.unique_mons['start'] = self.unique_mons['latest'] + + self.visits['latest'] = playerdata.get('poke_stop_visits', 0) + if self.visits['start'] is None: self.visits['start'] = self.visits['latest'] + + self.evolutions['latest'] = playerdata.get('evolutions', 0) + if self.evolutions['start'] is None: self.evolutions['start'] = self.evolutions['latest'] + except KeyError: + # Nothing we can do if there's no player info. + return diff --git a/pokemongo_bot/polyline_stepper.py b/pokemongo_bot/polyline_stepper.py deleted file mode 100644 index 32d07fc1c9..0000000000 --- a/pokemongo_bot/polyline_stepper.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- -from polyline_walker import PolylineWalker -from stepper import Stepper -from human_behaviour import sleep, random_lat_long_delta - - -class PolylineStepper(Stepper): - - def _walk_to(self, speed, lat, lng, alt): - origin = ','.join([str(self.api._position_lat), str(self.api._position_lng)]) - destination = ','.join([str(lat), str(lng)]) - polyline_walker = PolylineWalker(origin, destination, self.speed) - proposed_origin = polyline_walker.points[0] - proposed_destination = polyline_walker.points[-1] - proposed_lat = proposed_origin[0] - proposed_lng = proposed_origin[1] - if proposed_lat != lat and proposed_lng != lng: - self._old_walk_to(speed, proposed_lat, proposed_lng, alt) - while proposed_destination != polyline_walker.get_pos()[0]: - cLat, cLng = polyline_walker.get_pos()[0] - self.api.set_position(cLat, cLng, alt) - self.bot.heartbeat() - self._work_at_position(i2f(self.api._position_lat), i2f(self.api._position_lng), alt, False) - sleep(1) # sleep one second plus a random delta - if proposed_lat != self.api._position_lat and proposed_lng != self.api._position_lng: - self._old_walk_to(speed, lat, lng, alt) - - def _old_walk_to(self, speed, lat, lng, alt): - dist = distance( - i2f(self.api._position_lat), i2f(self.api._position_lng), lat, lng) - steps = (dist + 0.0) / (speed + 0.0) # may be rational number - intSteps = int(steps) - residuum = steps - intSteps - logger.log('[#] Walking from ' + str((i2f(self.api._position_lat), i2f( - self.api._position_lng))) + " to " + str(str((lat, lng))) + - " for approx. " + str(format_time(ceil(steps)))) - if steps != 0: - dLat = (lat - i2f(self.api._position_lat)) / steps - dLng = (lng - i2f(self.api._position_lng)) / steps - - for i in range(intSteps): - cLat = i2f(self.api._position_lat) + \ - dLat + random_lat_long_delta() - cLng = i2f(self.api._position_lng) + \ - dLng + random_lat_long_delta() - self.api.set_position(cLat, cLng, alt) - self.bot.heartbeat() - sleep(1) # sleep one second plus a random delta - self._work_at_position( - i2f(self.api._position_lat), i2f(self.api._position_lng), - alt, False) - - self.api.set_position(lat, lng, alt) - self.bot.heartbeat() - logger.log("[#] Finished walking") - diff --git a/pokemongo_bot/socketio_server/__init__.py b/pokemongo_bot/socketio_server/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pokemongo_bot/socketio_server/app.py b/pokemongo_bot/socketio_server/app.py new file mode 100644 index 0000000000..09c237f910 --- /dev/null +++ b/pokemongo_bot/socketio_server/app.py @@ -0,0 +1,32 @@ +import logging + +import socketio +from flask import Flask + + +sio = socketio.Server(async_mode='eventlet', logging=logging.NullHandler) +app = Flask(__name__) + +# client asks for data +@sio.on('remote:send_request') +def remote_control(sid, command): + if not 'account' in command: + return False + bot_name = command.pop('account') + event = 'bot:process_request:{}'.format(bot_name) + sio.emit(event, data=command) + +# sending bot response to client +@sio.on('bot:send_reply') +def request_reply(sid, response): + event = response.pop('command') + account = response.pop('account') + event = "{}:{}".format(event, account) + sio.emit(event, response) + +@sio.on('bot:broadcast') +def bot_broadcast(sid, env): + event = env.pop('event') + account = env.pop('account') + event_name = "{}:{}".format(event, account) + sio.emit(event_name, data=env['data']) diff --git a/pokemongo_bot/socketio_server/runner.py b/pokemongo_bot/socketio_server/runner.py new file mode 100644 index 0000000000..1a27fd8ff7 --- /dev/null +++ b/pokemongo_bot/socketio_server/runner.py @@ -0,0 +1,34 @@ +import threading + +import eventlet +import socketio +from eventlet import patcher, wsgi + +from app import app, sio + +patcher.monkey_patch(all=True) + + +class SocketIoRunner(object): + def __init__(self, url): + self.host, port_str = url.split(':') + self.port = int(port_str) + self.server = None + + # create the thread object + self.thread = threading.Thread(target=self._start_listening_blocking) + + # wrap Flask application with socketio's middleware + self.app = socketio.Middleware(sio, app) + + def start_listening_async(self): + wsgi.is_accepting = True + self.thread.start() + + def stop_listening(self): + wsgi.is_accepting = False + + def _start_listening_blocking(self): + # deploy as an eventlet WSGI server + listener = eventlet.listen((self.host, self.port)) + self.server = wsgi.server(listener, self.app, log_output=False, debug=False) diff --git a/pokemongo_bot/step_walker.py b/pokemongo_bot/step_walker.py new file mode 100644 index 0000000000..263699095d --- /dev/null +++ b/pokemongo_bot/step_walker.py @@ -0,0 +1,65 @@ +from math import sqrt + +from cell_workers.utils import distance +from human_behaviour import random_lat_long_delta, sleep + + +class StepWalker(object): + + def __init__(self, bot, speed, dest_lat, dest_lng): + self.bot = bot + self.api = bot.api + + self.initLat, self.initLng = self.bot.position[0:2] + + self.dist = distance( + self.initLat, + self.initLng, + dest_lat, + dest_lng + ) + + self.speed = speed + + self.destLat = dest_lat + self.destLng = dest_lng + self.totalDist = max(1, self.dist) + + self.steps = (self.dist + 0.0) / (speed + 0.0) + + if self.dist < speed or int(self.steps) <= 1: + self.dLat = 0 + self.dLng = 0 + self.magnitude = 0 + else: + self.dLat = (dest_lat - self.initLat) / int(self.steps) + self.dLng = (dest_lng - self.initLng) / int(self.steps) + self.magnitude = self._pythagorean(self.dLat, self.dLng) + + def step(self): + if (self.dLat == 0 and self.dLng == 0) or self.dist < self.speed: + self.api.set_position(self.destLat, self.destLng, 0) + return True + + totalDLat = (self.destLat - self.initLat) + totalDLng = (self.destLng - self.initLng) + magnitude = self._pythagorean(totalDLat, totalDLng) + unitLat = totalDLat / magnitude + unitLng = totalDLng / magnitude + + scaledDLat = unitLat * self.magnitude + scaledDLng = unitLng * self.magnitude + + cLat = self.initLat + scaledDLat + random_lat_long_delta() + cLng = self.initLng + scaledDLng + random_lat_long_delta() + + self.api.set_position(cLat, cLng, 0) + self.bot.heartbeat() + + sleep(1) # sleep one second plus a random delta + # self._work_at_position( + # self.initLat, self.initLng, + # alt, False) + + def _pythagorean(self, lat, lng): + return sqrt((lat ** 2) + (lng ** 2)) diff --git a/pokemongo_bot/stepper.py b/pokemongo_bot/stepper.py deleted file mode 100644 index fda5e63fb8..0000000000 --- a/pokemongo_bot/stepper.py +++ /dev/null @@ -1,158 +0,0 @@ -# -*- coding: utf-8 -*- - -import os -import json -import time -import pprint - -from math import ceil -from s2sphere import CellId, LatLng -from google.protobuf.internal import encoder - -from human_behaviour import sleep, random_lat_long_delta -from cell_workers.utils import distance, i2f, format_time - -from pgoapi.utilities import f2i, h2f -import logger - - -class Stepper(object): - def __init__(self, bot): - self.bot = bot - self.api = bot.api - self.config = bot.config - - self.pos = 1 - self.x = 0 - self.y = 0 - self.dx = 0 - self.dy = -1 - self.steplimit = self.config.max_steps - self.steplimit2 = self.steplimit**2 - self.origin_lat = self.bot.position[0] - self.origin_lon = self.bot.position[1] - - def take_step(self): - position = (self.origin_lat, self.origin_lon, 0.0) - - self.api.set_position(*position) - for step in range(self.steplimit2): - # starting at 0 index - logger.log('[#] Scanning area for objects ({} / {})'.format( - (step + 1), self.steplimit**2)) - if self.config.debug: - logger.log( - 'steplimit: {} x: {} y: {} pos: {} dx: {} dy {}'.format( - self.steplimit2, self.x, self.y, self.pos, self.dx, - self.dy)) - # Scan location math - if -self.steplimit2 / 2 < self.x <= self.steplimit2 / 2 and -self.steplimit2 / 2 < self.y <= self.steplimit2 / 2: - position = (self.x * 0.0025 + self.origin_lat, - self.y * 0.0025 + self.origin_lon, 0) - if self.config.walk > 0: - self._walk_to(self.config.walk, *position) - else: - self.api.set_position(*position) - print('[#] {}'.format(position)) - if self.x == self.y or self.x < 0 and self.x == -self.y or self.x > 0 and self.x == 1 - self.y: - (self.dx, self.dy) = (-self.dy, self.dx) - - (self.x, self.y) = (self.x + self.dx, self.y + self.dy) - - self._work_at_position(position[0], position[1], position[2], True) - sleep(10) - - def _walk_to(self, speed, lat, lng, alt): - dist = distance( - i2f(self.api._position_lat), i2f(self.api._position_lng), lat, lng) - steps = (dist + 0.0) / (speed + 0.0) # may be rational number - intSteps = int(steps) - residuum = steps - intSteps - logger.log('[#] Walking from ' + str((i2f(self.api._position_lat), i2f( - self.api._position_lng))) + " to " + str(str((lat, lng))) + - " for approx. " + str(format_time(ceil(steps)))) - if steps != 0: - dLat = (lat - i2f(self.api._position_lat)) / steps - dLng = (lng - i2f(self.api._position_lng)) / steps - - for i in range(intSteps): - cLat = i2f(self.api._position_lat) + \ - dLat + random_lat_long_delta() - cLng = i2f(self.api._position_lng) + \ - dLng + random_lat_long_delta() - self.api.set_position(cLat, cLng, alt) - self.bot.heartbeat() - sleep(1) # sleep one second plus a random delta - self._work_at_position( - i2f(self.api._position_lat), i2f(self.api._position_lng), - alt, False) - - self.api.set_position(lat, lng, alt) - self.bot.heartbeat() - logger.log("[#] Finished walking") - - def _work_at_position(self, lat, lng, alt, pokemon_only=False): - cellid = self._get_cellid(lat, lng) - timestamp = [0, ] * len(cellid) - self.api.get_map_objects(latitude=f2i(lat), - longitude=f2i(lng), - since_timestamp_ms=timestamp, - cell_id=cellid) - - response_dict = self.api.call() - # pprint.pprint(response_dict) - # Passing Variables through a file - if response_dict and 'responses' in response_dict: - if 'GET_MAP_OBJECTS' in response_dict['responses']: - if 'map_cells' in response_dict['responses'][ - 'GET_MAP_OBJECTS']: - user_web_location = 'web/location-%s.json' % (self.config.username) - if os.path.isfile(user_web_location): - with open(user_web_location, 'w') as outfile: - json.dump( - {'lat': lat, - 'lng': lng, - 'cells': response_dict[ - 'responses']['GET_MAP_OBJECTS']['map_cells']}, - outfile) - - user_data_lastlocation = 'data/last-location-%s.json' % (self.config.username) - if os.path.isfile(user_data_lastlocation): - with open(user_data_lastlocation, 'w') as outfile: - outfile.truncate() - json.dump({'lat': lat, 'lng': lng}, outfile) - - if response_dict and 'responses' in response_dict: - if 'GET_MAP_OBJECTS' in response_dict['responses']: - if 'status' in response_dict['responses']['GET_MAP_OBJECTS']: - if response_dict['responses']['GET_MAP_OBJECTS'][ - 'status'] is 1: - map_cells = response_dict['responses'][ - 'GET_MAP_OBJECTS']['map_cells'] - position = (lat, lng, alt) - # Sort all by distance from current pos- eventually this should build graph & A* it - # print(map_cells) - #print( s2sphere.from_token(x['s2_cell_id']) ) - map_cells.sort(key=lambda x: distance(lat, lng, x['forts'][0]['latitude'], x[ - 'forts'][0]['longitude']) if 'forts' in x and x['forts'] != [] else 1e6) - for cell in map_cells: - self.bot.work_on_cell(cell, position, pokemon_only) - - def _get_cellid(self, lat, long, radius=10): - origin = CellId.from_lat_lng(LatLng.from_degrees(lat, long)).parent(15) - walk = [origin.id()] - - # 10 before and 10 after - next = origin.next() - prev = origin.prev() - for i in range(radius): - walk.append(prev.id()) - walk.append(next.id()) - next = next.next() - prev = prev.prev() - return sorted(walk) - - def _encode(self, cellid): - output = [] - encoder._VarintEncoder()(output.append, cellid) - return ''.join(output) diff --git a/pokemongo_bot/test/__init__.py b/pokemongo_bot/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pokemongo_bot/test/follow_cluster_test.py b/pokemongo_bot/test/follow_cluster_test.py new file mode 100644 index 0000000000..4820a7558a --- /dev/null +++ b/pokemongo_bot/test/follow_cluster_test.py @@ -0,0 +1,42 @@ +import unittest, pickle, os +from mock import patch +from pokemongo_bot.cell_workers.follow_cluster import FollowCluster + + +class FollowClusterTestCase(unittest.TestCase): + + @patch('pokemongo_bot.PokemonGoBot') + def testWorkAway(self, mock_pokemongo_bot): + forts_path = os.path.join(os.path.dirname(__file__), 'resources', 'example_forts.pickle') + with open(forts_path, 'rb') as forts: + ex_forts = pickle.load(forts) + config = {'radius': 50, 'lured': False} + mock_pokemongo_bot.position = (37.396787, -5.994587) + mock_pokemongo_bot.config.walk = 4.16 + mock_pokemongo_bot.get_forts.return_value = ex_forts + follow_cluster = FollowCluster(mock_pokemongo_bot, config) + + expected = (37.397183750142624, -5.9932912500000013) + result = follow_cluster.work() + self.assertAlmostEqual(expected[0], result[0], delta=0.000000000010000) + self.assertAlmostEqual(expected[1], result[1], delta=0.000000000010000) + assert follow_cluster.is_at_destination == False + assert follow_cluster.announced == False + + @patch('pokemongo_bot.PokemonGoBot') + def testWorkArrived(self, mock_pokemongo_bot): + forts_path = os.path.join(os.path.dirname(__file__), 'resources', 'example_forts.pickle') + with open(forts_path, 'rb') as forts: + ex_forts = pickle.load(forts) + config = {'radius': 50, 'lured': False} + mock_pokemongo_bot.position = (37.39718375014263, -5.9932912500000013) + mock_pokemongo_bot.config.walk = 4.16 + mock_pokemongo_bot.get_forts.return_value = ex_forts + follow_cluster = FollowCluster(mock_pokemongo_bot, config) + + expected = (37.397183750142624, -5.9932912500000013) + result = follow_cluster.work() + self.assertAlmostEqual(expected[0], result[0], delta=0.000000000010000) + self.assertAlmostEqual(expected[1], result[1], delta=0.000000000010000) + assert follow_cluster.is_at_destination == True + assert follow_cluster.announced == False diff --git a/pokemongo_bot/test/resources/example_forts.pickle b/pokemongo_bot/test/resources/example_forts.pickle new file mode 100644 index 0000000000..83e516a845 --- /dev/null +++ b/pokemongo_bot/test/resources/example_forts.pickle @@ -0,0 +1,1415 @@ +(lp0 +(dp1 +S'last_modified_timestamp_ms' +p2 +L1469921475597L +sS'enabled' +p3 +I01 +sS'longitude' +p4 +F-5.993626 +sS'latitude' +p5 +F37.398013 +sS'type' +p6 +I1 +sS'id' +p7 +V251b8fed23fc423a830f90f2ba50c305.16 +p8 +sa(dp9 +g2 +L1469898328246L +sg3 +I01 +sg4 +F-5.9938 +sg5 +F37.397335 +sg6 +I1 +sg7 +V29e754fd9e364b1badcfbea89716f596.16 +p10 +sa(dp11 +g2 +L1469897243389L +sg3 +I01 +sg4 +F-5.993398 +sS'cooldown_complete_timestamp_ms' +p12 +L1469960466296L +sg5 +F37.397007 +sg6 +I1 +sg7 +V31cb76c028be429abf25c28a27fa5663.16 +p13 +sa(dp14 +g2 +L1469959534933L +sg3 +I01 +sg4 +F-5.991949 +sg5 +F37.397225 +sg6 +I1 +sg7 +V3459c81701da414e859cb0df5cde072f.16 +p15 +sa(dp16 +g2 +L1469374995437L +sg3 +I01 +sg4 +F-5.9924 +sg5 +F37.398369 +sg6 +I1 +sg7 +V4335073e77784d6cbeec259b707aee24.16 +p17 +sa(dp18 +g2 +L1469391332349L +sg3 +I01 +sg4 +F-5.991212 +sg5 +F37.396881 +sg6 +I1 +sg7 +V56c3ae94b6db4beb8d2a514f89b8d009.16 +p19 +sa(dp20 +g2 +L1469659319870L +sg3 +I01 +sg4 +F-5.992902 +sg12 +L1469960449891L +sg5 +F37.397015 +sg6 +I1 +sg7 +Va8ddb011a6bf4e528da833d7f8f1a573.16 +p21 +sa(dp22 +g2 +L1469778820610L +sg3 +I01 +sg4 +F-5.991572 +sg5 +F37.397664 +sg6 +I1 +sg7 +Vad48c742304149c691410af4104d6baa.16 +p23 +sa(dp24 +g2 +L1469643608920L +sg3 +I01 +sg4 +F-5.992462 +sg5 +F37.397701 +sg6 +I1 +sg7 +Ve79dbe80660549a0b3bd0dac15a99b4d.16 +p25 +sa(dp26 +g2 +L1469653812627L +sg3 +I01 +sg4 +F-5.993065 +sg12 +L1469960392023L +sg5 +F37.397378 +sg6 +I1 +sg7 +Vf5098ca9946c4d31aff4aab3875bf0f1.16 +p27 +sa(dp28 +g2 +L1469816066943L +sg3 +I01 +sg4 +F-5.99184 +sg5 +F37.395478 +sg6 +I1 +sg7 +V3918a8ebf8144446b27859e17362d238.16 +p29 +sa(dp30 +g2 +L1469733518512L +sg3 +I01 +sg4 +F-5.991272 +sg5 +F37.396256 +sg6 +I1 +sg7 +V3f51bfd8dc4b4984956374289f68059a.16 +p31 +sa(dp32 +g2 +L1469862148216L +sg3 +I01 +sg4 +F-5.991457 +sg5 +F37.39461 +sg6 +I1 +sg7 +V80c964060e494071ab456031ecc38718.16 +p33 +sa(dp34 +g2 +L1469833795206L +sg3 +I01 +sg4 +F-5.99319 +sg5 +F37.396199 +sg6 +I1 +sg7 +Va14d099f61d346cf91ca1bb18d6eeb6d.16 +p35 +sa(dp36 +g2 +L1469949486673L +sg3 +I01 +sg4 +F-5.992714 +sg5 +F37.39437 +sg6 +I1 +sg7 +Vda803c899d394a4aadd187d4572db120.16 +p37 +sa(dp38 +g2 +L1469963502716L +sg3 +I01 +sg4 +F-5.992074 +sg5 +F37.395383 +sg6 +I1 +sg7 +Vde9799520ce9438686a3a8edf1f82c6a.16 +p39 +sa(dp40 +g2 +L1469724649385L +sg3 +I01 +sg4 +F-5.99095 +sg5 +F37.398202 +sg6 +I1 +sg7 +V00a553c0856d4751b7b03ca5d935ef32.16 +p41 +sa(dp42 +g2 +L1469752401817L +sg3 +I01 +sg4 +F-5.989081 +sg5 +F37.398749 +sg6 +I1 +sg7 +V511826b74c2749beaf6a1a53ac9a6d6a.16 +p43 +sa(dp44 +g2 +L1469908392925L +sg3 +I01 +sg4 +F-5.989965 +sg5 +F37.398617 +sg6 +I1 +sg7 +V844891f9ff4342ef8ec3c9409a20da65.16 +p45 +sa(dp46 +g2 +L1469671694157L +sg3 +I01 +sg4 +F-5.990034 +sg5 +F37.399266 +sg6 +I1 +sg7 +Vdd74f0db5da84c4e90ffa0c1a9d6b206.16 +p47 +sa(dp48 +g2 +L1469528338773L +sg3 +I01 +sg4 +F-5.99098 +sg5 +F37.39896 +sg6 +I1 +sg7 +Ve276734ef68c4841b34fb66aada7eae1.16 +p49 +sa(dp50 +g2 +L1469158302943L +sg3 +I01 +sg4 +F-5.995018 +sg5 +F37.398226 +sg6 +I1 +sg7 +V80f84c3030e149f5bce2c5c2ca6058ba.16 +p51 +sa(dp52 +g2 +L1469910731097L +sg3 +I01 +sg4 +F-5.994533 +sg5 +F37.397635 +sg6 +I1 +sg7 +V8275d7d6b4ea44cf9656d6755715ba36.16 +p53 +sa(dp54 +g2 +L1469701402941L +sg3 +I01 +sg4 +F-5.995466 +sg5 +F37.398241 +sg6 +I1 +sg7 +V82860741d9434db58930be61817d5098.16 +p55 +sa(dp56 +g2 +L1469829182652L +sg3 +I01 +sg4 +F-5.994129 +sg5 +F37.399101 +sg6 +I1 +sg7 +V918befd170f54623868e51ce6a5d0ea9.11 +p57 +sa(dp58 +g2 +L1469908391471L +sg3 +I01 +sg4 +F-5.996385 +sg5 +F37.39721 +sg6 +I1 +sg7 +Vbd40e14e67fd4256845cca680866c3bf.16 +p59 +sa(dp60 +g2 +L1469918221191L +sg3 +I01 +sg4 +F-5.9941 +sg5 +F37.396979 +sg6 +I1 +sg7 +Vf5d1bb17378844ef8f5f181fd465b788.12 +p61 +sa(dp62 +g2 +L1469914861870L +sg3 +I01 +sg4 +F-5.99357 +sg5 +F37.400234 +sg6 +I1 +sg7 +V18712b17886140648e15a168b28810c0.12 +p63 +sa(dp64 +g2 +L1469730155405L +sg3 +I01 +sg4 +F-5.992908 +sg5 +F37.401593 +sg6 +I1 +sg7 +V49ec2372b10a4bf9b0667827cc9c7739.16 +p65 +sa(dp66 +g2 +L1469919202842L +sg3 +I01 +sg4 +F-5.993618 +sg5 +F37.400731 +sg6 +I1 +sg7 +V50ca2dbaae734ef98f69551dcc13052c.16 +p67 +sa(dp68 +g2 +L1469952945445L +sg3 +I01 +sg4 +F-5.991999 +sg5 +F37.400441 +sg6 +I1 +sg7 +V8a6f6662b93f4816b0749c15dfdf4118.16 +p69 +sa(dp70 +g2 +L1469963417085L +sg3 +I01 +sg4 +F-5.991508 +sg5 +F37.400873 +sg6 +I1 +sg7 +V9ab899ef25d842fb86ecf644e6668606.16 +p71 +sa(dp72 +g2 +L1469910941776L +sg3 +I01 +sg4 +F-5.993709 +sg5 +F37.399425 +sg6 +I1 +sg7 +Va4f7a938eb0a43cb9b67b095ad20a163.16 +p73 +sa(dp74 +g2 +L1469793537459L +sg3 +I01 +sg4 +F-5.991646 +sg5 +F37.399457 +sg6 +I1 +sg7 +Vc73349b5097348d4be65278c53a3a277.16 +p75 +sa(dp76 +g2 +L1468968208521L +sg3 +I01 +sg4 +F-5.990076 +sg5 +F37.39991 +sg6 +I1 +sg7 +V0259d9df56be4bcb960c44a774f75069.16 +p77 +sa(dp78 +g2 +L1469524106708L +sg3 +I01 +sg4 +F-5.990137 +sg5 +F37.400513 +sg6 +I1 +sg7 +V136c13edc75049b0b0f5cecef79a8afd.16 +p79 +sa(dp80 +g2 +L1469837426493L +sg3 +I01 +sg4 +F-5.990839 +sg5 +F37.399287 +sg6 +I1 +sg7 +V1ef6ea9edb3a4f64bdb833e5e205b5a3.16 +p81 +sa(dp82 +g2 +L1469938617325L +sg3 +I01 +sg4 +F-5.989291 +sg5 +F37.399426 +sg6 +I1 +sg7 +V74f43f0db4034f48be0aae963c50e3aa.16 +p83 +sa(dp84 +g2 +L1469962338544L +sg3 +I01 +sg4 +F-5.990811 +sg5 +F37.400943 +sg6 +I1 +sg7 +Va2797ccc46ce44e194adb3a19c69bd0a.16 +p85 +sa(dp86 +g2 +L1469557381652L +sg3 +I01 +sg4 +F-5.988956 +sg5 +F37.401278 +sg6 +I1 +sg7 +Vae554300c012425b8dc58240c4c80559.16 +p87 +sa(dp88 +g2 +L1469624514841L +sg3 +I01 +sg4 +F-5.990919 +sg5 +F37.400392 +sg6 +I1 +sg7 +Vbee12e766b114d2584de4e5f165416d7.16 +p89 +sa(dp90 +g2 +L1469917046183L +sg3 +I01 +sg4 +F-5.989457 +sg5 +F37.400447 +sg6 +I1 +sg7 +Ve96312dfd198478c88e70b1feed9a9e6.16 +p91 +sa(dp92 +g2 +L1469918359052L +sg3 +I01 +sg4 +F-5.990548 +sg5 +F37.401798 +sg6 +I1 +sg7 +Vef75f531b24a420e8c6c3cbfec68f11b.16 +p93 +sa(dp94 +g2 +L1469274803405L +sg3 +I01 +sg4 +F-5.989557 +sg5 +F37.394804 +sg6 +I1 +sg7 +V53fbb24493d14b15ad134c11adb3b0d4.16 +p95 +sa(dp96 +g2 +L1469740978120L +sg3 +I01 +sg4 +F-5.990471 +sg5 +F37.394447 +sg6 +I1 +sg7 +V7004401fb17145ddb12a131a81a177dd.16 +p97 +sa(dp98 +g2 +L1469554523140L +sg3 +I01 +sg4 +F-5.990258 +sg5 +F37.394936 +sg6 +I1 +sg7 +Vee8401b906bc4c4e90ac21bfb765f08d.16 +p99 +sa(dp100 +g2 +L1469232383352L +sg3 +I01 +sg4 +F-5.990434 +sg5 +F37.396289 +sg6 +I1 +sg7 +Vf1b217803ff14b6f8d92f79a592ae899.16 +p101 +sa(dp102 +g2 +L1469635023323L +sg3 +I01 +sg4 +F-5.988083 +sg5 +F37.39839 +sg6 +I1 +sg7 +V3e84fd1c22c94c1f98c8ad77750212f9.16 +p103 +sa(dp104 +g2 +L1467338046145L +sg3 +I01 +sg4 +F-5.987945 +sg5 +F37.398004 +sg6 +I1 +sg7 +V5cf76c6ab11542e3b0b95ccefc15165c.16 +p105 +sa(dp106 +g2 +L1469922064608L +sg3 +I01 +sg4 +F-5.987965 +sg5 +F37.396849 +sg6 +I1 +sg7 +Vdd031ddba0fb4325952c02675319383e.16 +p107 +sa(dp108 +g2 +L1469529349540L +sg3 +I01 +sg4 +F-5.988186 +sg5 +F37.398902 +sg6 +I1 +sg7 +Ve82311a4b53441e193e261136997c0c9.16 +p109 +sa(dp110 +g2 +L1469823571443L +sg3 +I01 +sg4 +F-5.994268 +sg5 +F37.401455 +sg6 +I1 +sg7 +Vd99b00cbb06845cc8499be1729c5795f.16 +p111 +sa(dp112 +g2 +L1469733713485L +sg3 +I01 +sg4 +F-5.995301 +sg5 +F37.400897 +sg6 +I1 +sg7 +Vddf11d8e4c7145b189b92b2735d0024f.16 +p113 +sa(dp114 +g2 +L1469800536694L +sg3 +I01 +sg4 +F-5.987715 +sg5 +F37.396294 +sg6 +I1 +sg7 +V0c7ca6878b564955ae64ee67583dfd12.16 +p115 +sa(dp116 +g2 +L1469873160929L +sg3 +I01 +sg4 +F-5.986963 +sg5 +F37.395385 +sg6 +I1 +sg7 +V2492e99faa3f46f687fd116944f0a1fd.16 +p117 +sa(dp118 +g2 +L1469877329921L +sg3 +I01 +sg4 +F-5.986343 +sg5 +F37.394868 +sg6 +I1 +sg7 +V553b3576e4f54eef8db0b6d2d91530fa.16 +p119 +sa(dp120 +g2 +L1467338046145L +sg3 +I01 +sg4 +F-5.985946 +sg5 +F37.396228 +sg6 +I1 +sg7 +V6caaf31aa22745f4a755532c71dedb9b.16 +p121 +sa(dp122 +g2 +L1469915504786L +sg3 +I01 +sg4 +F-5.986965 +sg5 +F37.396379 +sg6 +I1 +sg7 +Vb04109647b874d0aaa3de9512f12a3be.16 +p123 +sa(dp124 +g2 +L1469708371732L +sg3 +I01 +sg4 +F-5.999056 +sg5 +F37.396844 +sg6 +I1 +sg7 +V677f13616dae49b192ddde32b36e9ac1.16 +p125 +sa(dp126 +g2 +L1469826712933L +sg3 +I01 +sg4 +F-5.998739 +sg5 +F37.398765 +sg6 +I1 +sg7 +V78a89cc750924facb795ae525db994fc.16 +p127 +sa(dp128 +g2 +L1469908360503L +sg3 +I01 +sg4 +F-5.996781 +sg5 +F37.397039 +sg6 +I1 +sg7 +V7b76693d36e94a81b5f34f223ff1aaf3.11 +p129 +sa(dp130 +g2 +L1469907235683L +sg3 +I01 +sg4 +F-5.996629 +sg5 +F37.39686 +sg6 +I1 +sg7 +V8c4e707902b8451e981b252fff361a6a.16 +p131 +sa(dp132 +g2 +L1469556189588L +sg3 +I01 +sg4 +F-5.996728 +sg5 +F37.398118 +sg6 +I1 +sg7 +Vbc034ed05c32411692092a00f63a57b6.16 +p133 +sa(dp134 +g2 +L1469283047906L +sg3 +I01 +sg4 +F-5.998928 +sg5 +F37.397511 +sg6 +I1 +sg7 +Ve42813ce3cd843128c78c3d899986efa.16 +p135 +sa(dp136 +g2 +L1469871321651L +sg3 +I01 +sg4 +F-5.990257 +sg5 +F37.392608 +sg6 +I1 +sg7 +V091a36bea52f4cd9966c3a731e4ea9ff.11 +p137 +sa(dp138 +g2 +L1469821135378L +sg3 +I01 +sg4 +F-5.988966 +sg5 +F37.392712 +sg6 +I1 +sg7 +V24f958ae33b641b3b671484cf05c37d2.16 +p139 +sa(dp140 +g2 +L1469870325686L +sg3 +I01 +sg4 +F-5.991149 +sg5 +F37.39207 +sg6 +I1 +sg7 +V70ba833513274dc8bc558f79479fe980.16 +p141 +sa(dp142 +g2 +L1469653517541L +sg3 +I01 +sg4 +F-5.991137 +sg5 +F37.392742 +sg6 +I1 +sg7 +V83b1276113e149a6877f5cf73115680f.16 +p143 +sa(dp144 +g2 +L1469824630387L +sg3 +I01 +sg4 +F-5.989751 +sg5 +F37.391884 +sg6 +I1 +sg7 +Vb3770ec05c9147eaab4fe62672f005bf.16 +p145 +sa(dp146 +g2 +L1469649071116L +sg3 +I01 +sg4 +F-5.990481 +sg5 +F37.391728 +sg6 +I1 +sg7 +Vfe6454150fd1422b8ad1fd5c8a3a7c40.11 +p147 +sa(dp148 +g2 +L1467338046145L +sg3 +I01 +sg4 +F-5.993713 +sg5 +F37.391965 +sg6 +I1 +sg7 +V139ac87bc96b415dbbe74c669eb3be72.16 +p149 +sa(dp150 +g2 +L1469827559172L +sg3 +I01 +sg4 +F-5.992554 +sg5 +F37.392737 +sg6 +I1 +sg7 +V177e45b2924e4311b4665a69457f71ae.16 +p151 +sa(dp152 +g2 +L1469881093724L +sg3 +I01 +sg4 +F-5.993334 +sg5 +F37.393651 +sg6 +I1 +sg7 +V1c530a0dfd084ff0954b83a8531f6622.16 +p153 +sa(dp154 +g2 +L1469832326396L +sg3 +I01 +sg4 +F-5.993711 +sg5 +F37.392736 +sg6 +I1 +sg7 +V2370a972f47b488a84b4c383b8a03b53.11 +p155 +sa(dp156 +g2 +L1467338046145L +sg3 +I01 +sg4 +F-5.99289 +sg5 +F37.392406 +sg6 +I1 +sg7 +V6d7a5bb65edf4b0caec439770b4b032d.16 +p157 +sa(dp158 +g2 +L1469918510112L +sg3 +I01 +sg4 +F-5.991879 +sg5 +F37.392437 +sg6 +I1 +sg7 +Vb7beef68d23c4dc7afc2bbbb5c8034d9.16 +p159 +sa(dp160 +g2 +L1467338046146L +sg3 +I01 +sg4 +F-5.997456 +sg5 +F37.401344 +sg6 +I1 +sg7 +V20c49edbf515435b930ba5ab174e6cef.16 +p161 +sa(dp162 +g2 +L1467338046146L +sg3 +I01 +sg4 +F-5.997457 +sg5 +F37.400579 +sg6 +I1 +sg7 +V4df60f277b2f4a279e00178f6943d91e.16 +p163 +sa(dp164 +g2 +L1469650281423L +sg3 +I01 +sg4 +F-5.996481 +sg5 +F37.400605 +sg6 +I1 +sg7 +V975f2575f377477cabf0b8f5b8ad8d24.16 +p165 +sa(dp166 +g2 +L1469664434366L +sg3 +I01 +sg4 +F-6.000522 +sg5 +F37.39915 +sg6 +I1 +sg7 +V9cb368ad5e5842a5807761bac0df2361.11 +p167 +sa(dp168 +g2 +L1469704243449L +sg3 +I01 +sg4 +F-6.0 +sg5 +F37.399723 +sg6 +I1 +sg7 +Va1fe11bcbb844b7fb14d77634c8eba0b.16 +p169 +sa(dp170 +g2 +L1469922802991L +sg3 +I01 +sg4 +F-5.986843 +sg5 +F37.393024 +sg6 +I1 +sg7 +V41255acb114042c4ba8621174167b7a3.16 +p171 +sa(dp172 +g2 +L1469733237934L +sg3 +I01 +sg4 +F-5.988198 +sg5 +F37.394002 +sg6 +I1 +sg7 +V7d03c69e92a2464fb1b0c2fbb1dab905.12 +p173 +sa(dp174 +g2 +L1469729857242L +sg3 +I01 +sg4 +F-5.986549 +sg5 +F37.39156 +sg6 +I1 +sg7 +Vb3c32ce075c04a2a8b113a681468c8f6.16 +p175 +sa(dp176 +g2 +L1469799670291L +sg3 +I01 +sg4 +F-5.98837 +sg5 +F37.393029 +sg6 +I1 +sg7 +Vcd2009e3bf334527a81216c4ed00f455.16 +p177 +sa(dp178 +g2 +L1469922734509L +sg3 +I01 +sg4 +F-5.986344 +sg5 +F37.393054 +sg6 +I1 +sg7 +Vd81d562a7e57448fb7a7f074bd419e98.16 +p179 +sa(dp180 +g2 +L1469824608332L +sg3 +I01 +sg4 +F-5.987611 +sg5 +F37.393942 +sg6 +I1 +sg7 +Ve328724e66eb4ce7a0adeee5a6a40127.16 +p181 +sa(dp182 +g2 +L1469826878797L +sg3 +I01 +sg4 +F-5.986986 +sg5 +F37.391947 +sg6 +I1 +sg7 +Vf8863770b4774049b7557dd85fbda4f1.16 +p183 +sa(dp184 +g2 +L1469801146432L +sg3 +I01 +sg4 +F-5.984757 +sg5 +F37.399149 +sg6 +I1 +sg7 +V09cb0b980c2942b783b5e8f3f907428e.16 +p185 +sa(dp186 +g2 +L1469920128121L +sg3 +I01 +sg4 +F-5.985529 +sg5 +F37.399078 +sg6 +I1 +sg7 +V18d3084c92cc4dd4ba27cc3eaf7dd315.16 +p187 +sa(dp188 +g2 +L1469900596799L +sg3 +I01 +sg4 +F-5.983342 +sg5 +F37.397584 +sg6 +I1 +sg7 +V5efdf524b7c24b5484be94252f355638.16 +p189 +sa(dp190 +g2 +L1469920135971L +sg3 +I01 +sg4 +F-5.985615 +sg5 +F37.398821 +sg6 +I1 +sg7 +V73d88cf75bc542e5b86c1f50fbfc13e1.11 +p191 +sa(dp192 +g2 +L1469892832049L +sg3 +I01 +sg4 +F-5.984188 +sg5 +F37.397536 +sg6 +I1 +sg7 +V8758bdd8f9774ec2bc2f1272faa81198.16 +p193 +sa(dp194 +g2 +L1469960321243L +sg3 +I01 +sg4 +F-6.002275 +sg5 +F37.397373 +sg6 +I1 +sg7 +V38603d571ef14e93ae94ee3b95366f04.16 +p195 +sa(dp196 +g2 +L1469912143387L +sg3 +I01 +sg4 +F-6.001769 +sg5 +F37.398108 +sg6 +I1 +sg7 +Va4ff6d35307c4994b0e3106805a990f4.16 +p197 +sa(dp198 +g2 +L1469293296262L +sg3 +I01 +sg4 +F-5.983975 +sg5 +F37.394835 +sg6 +I1 +sg7 +Vbbe12779551b4dacaf7e0ed219dc841a.16 +p199 +sa(dp200 +g2 +L1469539921656L +sg3 +I01 +sg4 +F-5.984555 +sg5 +F37.39584 +sg6 +I1 +sg7 +Vdda84e7d0aa64b74b74adfe730058cde.16 +p201 +sa(dp202 +g2 +L1469450065604L +sg3 +I01 +sg4 +F-5.985628 +sg5 +F37.395615 +sg6 +I1 +sg7 +Vee073b5551df4b4b82c2e1dbd62394ae.16 +p203 +sa(dp204 +g2 +L1469916347191L +sg3 +I01 +sg4 +F-5.985723 +sg5 +F37.392219 +sg6 +I1 +sg7 +V6024589481e24862a458d6e131fc88d4.16 +p205 +sa(dp206 +g2 +L1469904426841L +sg3 +I01 +sg4 +F-5.983613 +sg5 +F37.394012 +sg6 +I1 +sg7 +V65237b304b404cef8ea8b65b1f9ebd95.16 +p207 +sa. \ No newline at end of file diff --git a/pokemongo_bot/test/sleep_schedule_test.py b/pokemongo_bot/test/sleep_schedule_test.py new file mode 100644 index 0000000000..05c35afbb6 --- /dev/null +++ b/pokemongo_bot/test/sleep_schedule_test.py @@ -0,0 +1,107 @@ +import unittest +from datetime import timedelta, datetime +from mock import patch, MagicMock +from pokemongo_bot.cell_workers.sleep_schedule import SleepSchedule +from tests import FakeBot + + +class SleepScheculeTestCase(unittest.TestCase): + config = {'time': '12:20', 'duration': '01:05', 'time_random_offset': '00:05', 'duration_random_offset': '00:05'} + + def setUp(self): + self.bot = FakeBot() + self.worker = SleepSchedule(self.bot, self.config) + + def test_config(self): + self.assertEqual(self.worker.time.hour, 12) + self.assertEqual(self.worker.time.minute, 20) + self.assertEqual(self.worker.duration, timedelta(hours=1, minutes=5).total_seconds()) + self.assertEqual(self.worker.time_random_offset, timedelta(minutes=5).total_seconds()) + self.assertEqual(self.worker.duration_random_offset, timedelta(minutes=5).total_seconds()) + + @patch('pokemongo_bot.cell_workers.sleep_schedule.datetime') + def test_get_next_time(self, mock_datetime): + mock_datetime.now.return_value = datetime(year=2016, month=8, day=01, hour=8, minute=0) + + next_time = self.worker._get_next_sleep_schedule() + from_date = datetime(year=2016, month=8, day=1, hour=12, minute=15) + to_date = datetime(year=2016, month=8, day=1, hour=12, minute=25) + + self.assertGreaterEqual(next_time, from_date) + self.assertLessEqual(next_time, to_date) + + @patch('pokemongo_bot.cell_workers.sleep_schedule.datetime') + def test_get_next_time_called_near_activation_time(self, mock_datetime): + mock_datetime.now.return_value = datetime(year=2016, month=8, day=1, hour=12, minute=25) + + next = self.worker._get_next_sleep_schedule() + from_date = datetime(year=2016, month=8, day=02, hour=12, minute=15) + to_date = datetime(year=2016, month=8, day=02, hour=12, minute=25) + + self.assertGreaterEqual(next, from_date) + self.assertLessEqual(next, to_date) + + @patch('pokemongo_bot.cell_workers.sleep_schedule.datetime') + def test_get_next_time_called_when_this_days_time_passed(self, mock_datetime): + mock_datetime.now.return_value = datetime(year=2016, month=8, day=1, hour=14, minute=0) + + next = self.worker._get_next_sleep_schedule() + from_date = datetime(year=2016, month=8, day=02, hour=12, minute=15) + to_date = datetime(year=2016, month=8, day=02, hour=12, minute=25) + + self.assertGreaterEqual(next, from_date) + self.assertLessEqual(next, to_date) + + def test_get_next_duration(self): + from_seconds = int(timedelta(hours=1).total_seconds()) + to_seconds = int(timedelta(hours=1, minutes=10).total_seconds()) + + duration = self.worker._get_next_duration() + + self.assertGreaterEqual(duration, from_seconds) + self.assertLessEqual(duration, to_seconds) + + @patch('pokemongo_bot.cell_workers.sleep_schedule.sleep') + def test_sleep(self, mock_sleep): + self.worker._next_duration = SleepSchedule.LOG_INTERVAL_SECONDS * 10 + self.worker._sleep() + #Sleep should be called 10 times with LOG_INTERVAL_SECONDS as argument + self.assertEqual(mock_sleep.call_count, 10) + calls = [x[0][0] for x in mock_sleep.call_args_list] + for arg in calls: + self.assertEqual(arg, SleepSchedule.LOG_INTERVAL_SECONDS) + + @patch('pokemongo_bot.cell_workers.sleep_schedule.sleep') + def test_sleep_not_divedable_by_interval(self, mock_sleep): + self.worker._next_duration = SleepSchedule.LOG_INTERVAL_SECONDS * 10 + 5 + self.worker._sleep() + self.assertEqual(mock_sleep.call_count, 11) + + calls = [x[0][0] for x in mock_sleep.call_args_list] + for arg in calls[:-1]: + self.assertEqual(arg, SleepSchedule.LOG_INTERVAL_SECONDS) + #Last call must be 5 + self.assertEqual(calls[-1], 5) + + @patch('pokemongo_bot.cell_workers.sleep_schedule.sleep') + @patch('pokemongo_bot.cell_workers.sleep_schedule.datetime') + def test_call_work_before_schedule(self, mock_datetime, mock_sleep): + self.worker._next_sleep = datetime(year=2016, month=8, day=1, hour=12, minute=0) + mock_datetime.now.return_value = self.worker._next_sleep - timedelta(minutes=5) + + self.worker.work() + + self.assertEqual(mock_sleep.call_count, 0) + + @patch('pokemongo_bot.cell_workers.sleep_schedule.sleep') + @patch('pokemongo_bot.cell_workers.sleep_schedule.datetime') + def test_call_work_after_schedule(self, mock_datetime, mock_sleep): + self.bot.login = MagicMock() + self.worker._next_sleep = datetime(year=2016, month=8, day=1, hour=12, minute=0) + # Change time to be after schedule + mock_datetime.now.return_value = self.worker._next_sleep + timedelta(minutes=5) + + self.worker.work() + + self.assertGreater(mock_sleep.call_count, 0) + self.assertGreater(self.bot.login.call_count, 0) diff --git a/pokemongo_bot/test/socketio-client.py b/pokemongo_bot/test/socketio-client.py new file mode 100644 index 0000000000..d30feee58d --- /dev/null +++ b/pokemongo_bot/test/socketio-client.py @@ -0,0 +1,15 @@ +from socketIO_client import SocketIO + + +def on_location(msg): + print('received location: {}'.format(msg)) + +if __name__ == "__main__": + try: + socketio = SocketIO('localhost', 4000) + socketio.on('location', on_location) + while True: + socketio.wait(seconds=5) + + except (KeyboardInterrupt, SystemExit): + print "Exiting" diff --git a/pokemongo_bot/tree_config_builder.py b/pokemongo_bot/tree_config_builder.py new file mode 100644 index 0000000000..bba63ad880 --- /dev/null +++ b/pokemongo_bot/tree_config_builder.py @@ -0,0 +1,36 @@ +import cell_workers + +class ConfigException(Exception): + pass + +class TreeConfigBuilder(object): + def __init__(self, bot, tasks_raw): + self.bot = bot + self.tasks_raw = tasks_raw + + def _get_worker_by_name(self, name): + try: + worker = getattr(cell_workers, name) + except AttributeError: + raise ConfigException('No worker named {} defined'.format(name)) + + return worker + + def build(self): + workers = [] + + for task in self.tasks_raw: + task_type = task.get('type', None) + if task_type is None: + raise ConfigException('No type found for given task {}'.format(task)) + elif task_type == 'EvolveAll': + raise ConfigException('The EvolveAll task has been renamed to EvolvePokemon') + + task_config = task.get('config', {}) + + worker = self._get_worker_by_name(task_type) + instance = worker(self.bot, task_config) + workers.append(instance) + + return workers + diff --git a/pokemongo_bot/polyline_walker/__init__.py b/pokemongo_bot/walkers/__init__.py similarity index 51% rename from pokemongo_bot/polyline_walker/__init__.py rename to pokemongo_bot/walkers/__init__.py index 272b8375d4..c7021b6d56 100644 --- a/pokemongo_bot/polyline_walker/__init__.py +++ b/pokemongo_bot/walkers/__init__.py @@ -1 +1,2 @@ +from polyline_generator import Polyline from polyline_walker import PolylineWalker diff --git a/pokemongo_bot/polyline_walker/polyline_walker.py b/pokemongo_bot/walkers/polyline_generator.py similarity index 51% rename from pokemongo_bot/polyline_walker/polyline_walker.py rename to pokemongo_bot/walkers/polyline_generator.py index e6a92c1c5e..ee225cb9e8 100644 --- a/pokemongo_bot/polyline_walker/polyline_walker.py +++ b/pokemongo_bot/walkers/polyline_generator.py @@ -1,23 +1,29 @@ -import requests -import polyline -import haversine import time -from itertools import chain +from itertools import chain from math import ceil -class PolylineWalker(object): +import haversine +import polyline +import requests + + +class Polyline(object): def __init__(self, origin, destination, speed): self.DISTANCE_API_URL='https://maps.googleapis.com/maps/api/directions/json?mode=walking' self.origin = origin self.destination = destination - self.polyline_points = [x['polyline']['points'] for x in - requests.get(self.DISTANCE_API_URL+'&origin='+ - self.origin+'&destination='+ - self.destination - ).json()['routes'][0]['legs'][0]['steps']] + self.URL = '{}&origin={}&destination={}'.format(self.DISTANCE_API_URL, + '{},{}'.format(*self.origin), + '{},{}'.format(*self.destination)) + self.request_responce = requests.get(self.URL).json() + try: + self.polyline_points = [x['polyline']['points'] for x in + self.request_responce['routes'][0]['legs'][0]['steps']] + except IndexError: + self.polyline_points = self.request_responce['routes'] self.speed = float(speed) - self.points = self.get_points(self.polyline_points) + self.points = [self.origin] + self.get_points(self.polyline_points) + [self.destination] self.lat, self.long = self.points[0][0], self.points[0][1] self.polyline = self.combine_polylines(self.points) self._timestamp = time.time() @@ -57,7 +63,8 @@ def walk_steps(self): walk_steps = zip(chain([self.points[0]], self.points), chain(self.points, [self.points[-1]])) walk_steps = filter(None, [(o, d) if o != d else None for o, d in walk_steps]) - return walk_steps + # consume the filter as list https://github.com/th3w4y/PokemonGo-Bot/issues/27 + return list(walk_steps) else: return [] @@ -68,26 +75,35 @@ def get_pos(self): else: time_passed = self._last_paused_timestamp time_passed_distance = self.speed * abs(time_passed - self._timestamp - self._paused_total) - steps_dict = {} - for step in self.walk_steps(): - walked_distance += haversine.haversine(*step)*1000 - steps_dict[walked_distance] = step - for walked_end_step in sorted(steps_dict.keys()): + # check if there are any steps to take https://github.com/th3w4y/PokemonGo-Bot/issues/27 + if self.walk_steps(): + steps_dict = {} + for step in self.walk_steps(): + walked_distance += haversine.haversine(*step)*1000 + steps_dict[walked_distance] = step + for walked_end_step in sorted(steps_dict.keys()): + if walked_end_step >= time_passed_distance: + break + step_distance = haversine.haversine(*steps_dict[walked_end_step])*1000 if walked_end_step >= time_passed_distance: - break - step_distance = haversine.haversine(*steps_dict[walked_end_step])*1000 - if walked_end_step >= time_passed_distance: - percentage_walked = (time_passed_distance - (walked_end_step - step_distance)) / step_distance + percentage_walked = (time_passed_distance - (walked_end_step - step_distance)) / step_distance + else: + percentage_walked = 1.0 + return self.calculate_coord(percentage_walked, *steps_dict[walked_end_step]) else: - percentage_walked = 1.0 - return self.calculate_coord(percentage_walked, *steps_dict[walked_end_step]) + # otherwise return the destination https://github.com/th3w4y/PokemonGo-Bot/issues/27 + return [self.points[-1]] def calculate_coord(self, percentage, o, d): - lat = o[0]+ (d[0] -o[0]) * percentage - lon = o[1]+ (d[1] -o[1]) * percentage - return [(round(lat, 5), round(lon, 5))] + # If this is the destination then returning as such + if self.points[-1] == d: + return [d] + else: + # intermediary points returned with 5 decimals precision only + # this ensures ~3-50cm ofset from the geometrical point calculated + lat = o[0]+ (d[0] -o[0]) * percentage + lon = o[1]+ (d[1] -o[1]) * percentage + return [(round(lat, 5), round(lon, 5))] def get_total_distance(self): return ceil(sum([haversine.haversine(*x)*1000 for x in self.walk_steps()])) - - diff --git a/pokemongo_bot/polyline_walker/polyline_tester.py b/pokemongo_bot/walkers/polyline_generator_tester.py similarity index 83% rename from pokemongo_bot/polyline_walker/polyline_tester.py rename to pokemongo_bot/walkers/polyline_generator_tester.py index fdf547fdd6..72852eebb9 100644 --- a/pokemongo_bot/polyline_walker/polyline_tester.py +++ b/pokemongo_bot/walkers/polyline_generator_tester.py @@ -1,9 +1,13 @@ import time +from math import ceil + import haversine import polyline -from math import ceil -from polyline_walker import PolylineWalker -a = PolylineWalker('Poststrasse+20,Zug,CH', 'Guggiweg+7,Zug,CH', 100) + +from polyline_generator import Polyline + +a = Polyline((47.1706378, 8.5167405), (47.1700271, 8.518072999999998), 100) +print(a.points) print('Walking polyline: ', a.polyline) print('Encoded level: ','B'*len(a.points)) print('Initialted with speed: ', a.speed, 'm/s') diff --git a/pokemongo_bot/walkers/polyline_walker.py b/pokemongo_bot/walkers/polyline_walker.py new file mode 100644 index 0000000000..687371e866 --- /dev/null +++ b/pokemongo_bot/walkers/polyline_walker.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.step_walker import StepWalker +from polyline_generator import Polyline + + +class PolylineWalker(StepWalker): + + def __init__(self, bot, speed, dest_lat, dest_lng): + super(PolylineWalker, self).__init__(bot, speed, dest_lat, dest_lng) + self.polyline_walker = Polyline((self.api._position_lat, self.api._position_lng), + (self.destLat, self.destLng), self.speed) + self.bot.event_manager.emit( + 'polyline_request', + sender=self, + level='info', + formatted="{url}", + data={'url': self.polyline_walker.URL} + ) + + def step(self): + cLat, cLng = self.api._position_lat, self.api._position_lng + while (cLat, cLng) != self.polyline_walker.get_pos()[0]: + self.polyline_walker.unpause() + sleep(1) + self.polyline_walker.pause() + cLat, cLng = self.polyline_walker.get_pos()[0] + self.api.set_position(round(cLat, 5), round(cLng, 5), 0) + self.bot.heartbeat() + return True diff --git a/pokemongo_bot/websocket_remote_control.py b/pokemongo_bot/websocket_remote_control.py new file mode 100644 index 0000000000..c4e15362b6 --- /dev/null +++ b/pokemongo_bot/websocket_remote_control.py @@ -0,0 +1,53 @@ +import threading +from socketIO_client import SocketIO, BaseNamespace + + +class WebsocketRemoteControl(object): + + + def __init__(self, bot): + self.bot = bot + self.host, port_str = self.bot.config.websocket_server_url.split(':') + self.port = int(port_str) + self.sio = SocketIO(self.host, self.port) + self.sio.on( + 'bot:process_request:{}'.format(self.bot.config.username), + self.on_remote_command + ) + self.thread = threading.Thread(target=self.process_messages) + + def start(self): + self.thread.start() + return self + + def process_messages(self): + self.sio.wait() + + def on_remote_command(self, command): + name = command['name'] + command_handler = getattr(self, name, None) + if not command_handler or not callable(command_handler): + self.sio.emit( + 'bot:send_reply', + { + 'response': '', + 'command': 'command_not_found', + 'account': self.bot.config.username + } + ) + return + if 'args' in command: + command_handler(*args) + return + command_handler() + + def get_player_info(self): + player_info = self.bot.get_inventory()['responses']['GET_INVENTORY'] + self.sio.emit( + 'bot:send_reply', + { + 'result': player_info, + 'command': 'get_player_info', + 'account': self.bot.config.username + } + ) diff --git a/pokemongo_bot/worker_result.py b/pokemongo_bot/worker_result.py new file mode 100644 index 0000000000..f38ceb9704 --- /dev/null +++ b/pokemongo_bot/worker_result.py @@ -0,0 +1,3 @@ +class WorkerResult(object): + RUNNING = 'RUNNING' + SUCCESS = 'SUCCESS' diff --git a/pylint-recursive.py b/pylint-recursive.py new file mode 100644 index 0000000000..82c4664e97 --- /dev/null +++ b/pylint-recursive.py @@ -0,0 +1,60 @@ +#! /usr/bin/env python +''' +Author: gregorynicholas (github), modified by Jacob Henderson (jacohend, github) +Module that runs pylint on all python scripts found in a directory tree.. +''' + +import os +#import re +import sys + +passed = 0 +failed = 0 +errors = list() + +IGNORED_FILES = ["lcd.py"] + +def check(module): + global passed, failed + ''' + apply pylint to the file specified if it is a *.py file + ''' + module_name = module.rsplit('/', 1)[1] + if module[-3:] == ".py" and module_name not in IGNORED_FILES: + print "CHECKING ", module + pout = os.popen('pylint %s'% module, 'r') + for line in pout: + if "Your code has been rated at" in line: + print "PASSED pylint inspection: " + line + passed += 1 + return True + if "-error" in line: + print "FAILED pylint inspection: " + line + failed += 1 + errors.append("FILE: " + module) + errors.append("FAILED pylint inspection: " + line) + return False + +if __name__ == "__main__": + try: + print sys.argv + BASE_DIRECTORY = sys.argv[1] + except IndexError: + print "no directory specified, defaulting to current working directory" + BASE_DIRECTORY = os.getcwd() + + print "looking for *.py scripts in subdirectories of ", BASE_DIRECTORY + + for root, dirs, files in os.walk(BASE_DIRECTORY): + for name in files: + filepath = os.path.join(root, name) + check(filepath) + + print "Passed: " + str(passed) + " Failed: " + str(failed) + print "\n" + print "Showing errors:" + if failed > 0: + for err in errors: + print err + + sys.exit("Pylint failed with errors") diff --git a/release_config.json.example b/release_config.json.example deleted file mode 100644 index 06d4687e9a..0000000000 --- a/release_config.json.example +++ /dev/null @@ -1,174 +0,0 @@ -{ - "any": { - "release_under_cp": 400, - "release_under_iv": 0.9, - "cp_iv_logic": "and" - }, - - "Bulbasaur": { "release_under_cp": 374, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Ivysaur": { "release_under_cp": 571, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Venusaur": { "release_under_cp": 902, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Charmander": { "release_under_cp": 333, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Charmeleon": { "release_under_cp": 544, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Charizard": { "release_under_cp": 909, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Squirtle": { "release_under_cp": 352, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Wartortle": { "release_under_cp": 552, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Blastoise": { "release_under_cp": 888, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Caterpie": { "release_under_cp": 156, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Metapod": { "release_under_cp": 168, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Butterfree": { "release_under_cp": 508, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Weedle": { "release_under_cp": 156, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Kakuna": { "release_under_cp": 170, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Beedrill": { "release_under_cp": 504, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Pidgey": { "release_under_cp": 237, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Pidgeotto": { "release_under_cp": 427, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Pidgeot": { "release_under_cp": 729, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Rattata": { "release_under_cp": 204, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Raticate": { "release_under_cp": 504, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Spearow": { "release_under_cp": 240, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Fearow": { "release_under_cp": 609, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Ekans": { "release_under_cp": 288, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Arbok": { "release_under_cp": 616, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Pikachu": { "release_under_cp": 309, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Raichu": { "release_under_cp": 708, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Sandshrew": { "release_under_cp": 278, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Sandslash": { "release_under_cp": 631, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Nidoran F": { "release_under_cp": 304, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Nidorina": { "release_under_cp": 489, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Nidoqueen": { "release_under_cp": 868, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Nidoran M": { "release_under_cp": 295, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Nidorino": { "release_under_cp": 480, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Nidoking": { "release_under_cp": 864, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Clefairy": { "release_under_cp": 420, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Clefable": { "release_under_cp": 837, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Vulpix": { "release_under_cp": 290, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Ninetales": { "release_under_cp": 763, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Jigglypuff": { "release_under_cp": 321, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Wigglytuff": { "release_under_cp": 760, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Zubat": { "release_under_cp": 225, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Golbat": { "release_under_cp": 672, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Oddish": { "release_under_cp": 400, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Gloom": { "release_under_cp": 590, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Vileplume": { "release_under_cp": 871, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Paras": { "release_under_cp": 319, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Parasect": { "release_under_cp": 609, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Venonat": { "release_under_cp": 360, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Venomoth": { "release_under_cp": 660, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Diglett": { "release_under_cp": 158, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Dugtrio": { "release_under_cp": 408, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Meowth": { "release_under_cp": 264, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Persian": { "release_under_cp": 568, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Psyduck": { "release_under_cp": 386, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Golduck": { "release_under_cp": 832, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Mankey": { "release_under_cp": 307, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Primeape": { "release_under_cp": 650, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Growlithe": { "release_under_cp": 465, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Arcanine": { "release_under_cp": 1041, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Poliwag": { "release_under_cp": 278, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Poliwhirl": { "release_under_cp": 468, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Poliwrath": { "release_under_cp": 876, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Abra": { "release_under_cp": 208, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Kadabra": { "release_under_cp": 396, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Alakazam": { "release_under_cp": 633, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Machop": { "release_under_cp": 381, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Machoke": { "release_under_cp": 614, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Machamp": { "release_under_cp": 907, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Bellsprout": { "release_under_cp": 391, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Weepinbell": { "release_under_cp": 602, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Victreebel": { "release_under_cp": 883, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Tentacool": { "release_under_cp": 316, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Tentacruel": { "release_under_cp": 775, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Geodude": { "release_under_cp": 297, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Graveler": { "release_under_cp": 501, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Golem": { "release_under_cp": 804, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Ponyta": { "release_under_cp": 530, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Rapidash": { "release_under_cp": 768, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Slowpoke": { "release_under_cp": 424, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Slowbro": { "release_under_cp": 907, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Magnemite": { "release_under_cp": 312, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Magneton": { "release_under_cp": 657, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Farfetch'd": { "release_under_cp": 441, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Doduo": { "release_under_cp": 297, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Dodrio": { "release_under_cp": 640, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Seel": { "release_under_cp": 386, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Dewgong": { "release_under_cp": 748, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Grimer": { "release_under_cp": 448, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Muk": { "release_under_cp": 909, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Shellder": { "release_under_cp": 288, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Cloyster": { "release_under_cp": 717, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Gastly": { "release_under_cp": 280, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Haunter": { "release_under_cp": 482, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Gengar": { "release_under_cp": 724, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Onix": { "release_under_cp": 300, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Drowzee": { "release_under_cp": 374, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Hypno": { "release_under_cp": 763, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Krabby": { "release_under_cp": 276, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Kingler": { "release_under_cp": 636, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Voltorb": { "release_under_cp": 292, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Electrode": { "release_under_cp": 576, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Exeggcute": { "release_under_cp": 384, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Exeggutor": { "release_under_cp": 1032, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Cubone": { "release_under_cp": 352, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Marowak": { "release_under_cp": 578, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Hitmonlee": { "release_under_cp": 520, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Hitmonchan": { "release_under_cp": 530, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Lickitung": { "release_under_cp": 568, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Koffing": { "release_under_cp": 403, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Weezing": { "release_under_cp": 784, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Rhyhorn": { "release_under_cp": 412, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Rhydon": { "release_under_cp": 782, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Chansey": { "release_under_cp": 235, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Tangela": { "release_under_cp": 607, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Kangaskhan": { "release_under_cp": 712, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Horsea": { "release_under_cp": 278, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Seadra": { "release_under_cp": 597, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Goldeen": { "release_under_cp": 336, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Seaking": { "release_under_cp": 712, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Staryu": { "release_under_cp": 326, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Starmie": { "release_under_cp": 763, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Mr. Mime": { "release_under_cp": 520, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Scyther": { "release_under_cp": 724, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Jynx": { "release_under_cp": 600, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Electabuzz": { "release_under_cp": 739, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Magmar": { "release_under_cp": 792, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Pinsir": { "release_under_cp": 741, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Tauros": { "release_under_cp": 643, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Magikarp": { "release_under_cp": 91, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Gyarados": { "release_under_cp": 938, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Lapras": { "release_under_cp": 1041, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Ditto": { "release_under_cp": 321, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Eevee": { "release_under_cp": 376, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Vaporeon": { "release_under_cp": 984, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Jolteon": { "release_under_cp": 746, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Flareon": { "release_under_cp": 924, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Porygon": { "release_under_cp": 590, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Omanyte": { "release_under_cp": 391, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Omastar": { "release_under_cp": 780, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Kabuto": { "release_under_cp": 386, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Kabutops": { "release_under_cp": 744, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Aerodactyl": { "release_under_cp": 756, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Snorlax": { "release_under_cp": 1087, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Articuno": { "release_under_cp": 1039, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Zapdos": { "release_under_cp": 1087, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Moltres": { "release_under_cp": 1132, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Dratini": { "release_under_cp": 343, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Dragonair": { "release_under_cp": 609, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Dragonite": { "release_under_cp": 1221, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Mewtwo": { "release_under_cp": 1447, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Mew": { "release_under_cp": 1152, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - - "exceptions": { - "always_capture": [ - "Arcanine", - "Lapras", - "Dragonite", - "Snorlax", - "Blastoise", - "Moltres", - "Articuno", - "Zapdos", - "Mew", - "Mewtwo" - ] - } -} diff --git a/requirements.txt b/requirements.txt index 5c5223bf71..d8b5ce5c9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ --e git+https://github.com/tejado/pgoapi.git@3787ffbe2e80ebce8a02d48eebceb9edf40179c1#egg=pgoapi +numpy==1.11.0 +networkx==1.11 +-e git+https://github.com/tejado/pgoapi.git@0811db23d639039f968a82e06c7aa15a0a5016b6#egg=pgoapi geopy==1.11.0 protobuf==3.0.0b4 requests==2.10.0 @@ -11,3 +13,11 @@ enum34==1.1.6 pyyaml==3.11 haversine==0.4.5 polyline==1.3.1 +python-socketio==1.4.2 +flask==0.11.1 +socketIO_client==0.7.0 +eventlet==0.19.0 +gpxpy==1.1.1 +mock==2.0.0 +timeout-decorator==0.3.2 +raven==5.23.0 diff --git a/run.sh b/run.sh new file mode 100755 index 0000000000..ec95acb3e3 --- /dev/null +++ b/run.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# Starts PokemonGo-Bot +config="" + +if [ ! -z $1 ]; then + config=$1 +else + config="./configs/config.json" + if [ ! -f ${config} ]; then + echo -e "There's no ./configs/config.json file" + echo -e "Please create one or use another config file" + echo -e "./run.sh [path/to/config/file]" + exit 1 + fi +fi + +python pokecli.py --config ${config} diff --git a/setup.py b/setup.py index f0a267c497..6ccf893c0d 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ #!/usr/bin/env python -from distutils.core import setup from pip.req import parse_requirements install_reqs = parse_requirements("requirements.txt", session=False) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..c02aed60fc --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,23 @@ +# __init__.py +from mock import MagicMock + +from pokemongo_bot.event_manager import EventManager +from pokemongo_bot.api_wrapper import ApiWrapper, ApiRequest +from pokemongo_bot import PokemonGoBot + +class FakeApi(ApiWrapper): + 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): + self.config = MagicMock(websocket_server_url=False, show_events=False) + self.api = FakeApi() + self.event_manager = EventManager() + self._setup_event_system() + + def updateConfig(self, conf): + self.config.__dict__.update(conf) diff --git a/tests/api_wrapper_test.py b/tests/api_wrapper_test.py new file mode 100644 index 0000000000..335f04cbf2 --- /dev/null +++ b/tests/api_wrapper_test.py @@ -0,0 +1,123 @@ +import unittest +from mock import MagicMock, patch +from timeout_decorator import timeout, TimeoutError + +from tests import FakeApi + +from pgoapi import PGoApi +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() + api.set_position(*(42, 42, 0)) + request = api.create_request() + request.get_inventory(test='awesome') + with self.assertRaises(NotLoggedInException): + request.call() + + def test_api_call_with_no_requests_set(self): + 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 + 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): + request.call() + + def test_mocked_call(self): + 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): + api = FakeApi() + def returnRequest(ret_value): + request = api.create_request(ret_value) + request.get_inventory(test='awesome') + return request + + wrong_return_values = [ + None, + False, + {}, + {'responses': {}}, + {'status_code': 0}, + {'responses': {'GET_INVENTORY_OR_NOT': {}}, 'status_code': 0} + ] + for wrong in wrong_return_values: + request = returnRequest(wrong) + request_callers = request._pop_request_callers() # we can pop because we do no call + + 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): + request = FakeApi().create_request() # we set the return value below + request.get_inventory(test='awesome') + + request_caller = request.request_callers[0] # only one request + self.assertEqual(request_caller.upper(), 'GET_INVENTORY') + + good_return_value = {'responses': {request_caller.upper(): {}}, 'status_code': 0} + request._call.return_value = good_return_value + + result = request.call() + self.assertEqual(result, good_return_value) + self.assertEqual(len(request.request_callers), 0, 'request_callers must be empty') + + def test_multiple_requests(self): + request = FakeApi().create_request() + request.get_inventory(test='awesome') + request.fort_details() + + good_return_value = {'responses': {'GET_INVENTORY': {}, 'FORT_DETAILS': {}}, 'status_code': 0} + request._call.return_value = good_return_value + + result = request.call() + self.assertEqual(result, good_return_value) + + @timeout(1) + def test_api_call_throttle_should_pass(self): + request = FakeApi().create_request() + request.is_response_valid = MagicMock(return_value=True) + request.requests_per_seconds = 5 + + for i in range(request.requests_per_seconds): + request.call() + + @timeout(1) # expects a timeout + def test_api_call_throttle_should_fail(self): + 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(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/tests/base_task_test.py b/tests/base_task_test.py new file mode 100644 index 0000000000..16684d900c --- /dev/null +++ b/tests/base_task_test.py @@ -0,0 +1,40 @@ +import unittest +import json +from pokemongo_bot.cell_workers import BaseTask + +class FakeTask(BaseTask): + def initialize(self): + self.foo = 'foo' + + def work(self): + pass + +class FakeTaskWithoutInitialize(BaseTask): + def work(self): + pass + +class FakeTaskWithoutWork(BaseTask): + pass + +class BaseTaskTest(unittest.TestCase): + def setUp(self): + self.bot = {} + self.config = {} + + def test_initialize_called(self): + task = FakeTask(self.bot, self.config) + self.assertIs(task.bot, self.bot) + self.assertIs(task.config, self.config) + self.assertEquals(task.foo, 'foo') + + def test_does_not_throw_without_initialize(self): + FakeTaskWithoutInitialize(self.bot, self.config) + + def test_throws_without_work(self): + self.assertRaisesRegexp( + NotImplementedError, + 'Missing "work" method', + FakeTaskWithoutWork, + self.bot, + self.config + ) diff --git a/tests/location_parser_test.py b/tests/location_parser_test.py new file mode 100644 index 0000000000..e724adebf8 --- /dev/null +++ b/tests/location_parser_test.py @@ -0,0 +1,33 @@ +# coding: utf-8 +import unittest +from mock import MagicMock + +from geopy.exc import GeocoderQueryError +from tests import FakeBot + + +class TestLocationParser(unittest.TestCase): + + def setUp(self): + self.bot = FakeBot() + config = dict( + test=False, + location='Paris', + location_cache=False, + username='Foobar', + ) + self.bot.updateConfig(config) + + def test_named_position(self): + position = (42, 42, 0) + self.bot.get_pos_by_name = MagicMock(return_value=position) + self.bot._set_starting_position() + self.assertEqual(self.bot.position, position) + + def test_named_position_utf8(self): + position = (42, 42, 0) + self.bot.config.location = u"àéùƱǣЊ؍ ข᠃" + self.bot.get_pos_by_name = MagicMock(return_value=position) + + self.bot._set_starting_position() + self.assertEqual(self.bot.position, position) diff --git a/tests/step_walker_test.py b/tests/step_walker_test.py new file mode 100644 index 0000000000..7472953ac6 --- /dev/null +++ b/tests/step_walker_test.py @@ -0,0 +1,73 @@ +import unittest +from mock import MagicMock, patch + +from pokemongo_bot.step_walker import StepWalker +from pokemongo_bot.cell_workers.utils import float_equal + +NORMALIZED_LAT_LNG_DISTANCE_STEP = 6.3593e-6 + +class TestStepWalker(unittest.TestCase): + def setUp(self): + self.patcherSleep = patch('pokemongo_bot.step_walker.sleep') + self.patcherRandomLat = patch('pokemongo_bot.step_walker.random_lat_long_delta', return_value=0) + self.patcherSleep.start() + self.patcherRandomLat.start() + + self.bot = MagicMock() + self.bot.position = [0, 0, 0] + self.bot.api = MagicMock() + + self.lat, self.lng, self.alt = 0, 0, 0 + + # let us get back the position set by the StepWalker + def api_set_position(lat, lng, alt): + self.lat, self.lng, self.alt = lat, lng, alt + self.bot.api.set_position = api_set_position + + def tearDown(self): + self.patcherSleep.stop() + self.patcherRandomLat.stop() + + def test_normalized_distance(self): + sw = StepWalker(self.bot, 1, 0.1, 0.1) + self.assertGreater(sw.dLat, 0) + self.assertGreater(sw.dLng, 0) + + stayInPlace = sw.step() + self.assertFalse(stayInPlace) + + self.assertTrue(float_equal(self.lat, NORMALIZED_LAT_LNG_DISTANCE_STEP)) + self.assertTrue(float_equal(self.lng, NORMALIZED_LAT_LNG_DISTANCE_STEP)) + + def test_normalized_distance_times_2(self): + sw = StepWalker(self.bot, 2, 0.1, 0.1) + self.assertTrue(sw.dLat > 0) + self.assertTrue(sw.dLng > 0) + + stayInPlace = sw.step() + self.assertFalse(stayInPlace) + + self.assertTrue(float_equal(self.lat, NORMALIZED_LAT_LNG_DISTANCE_STEP * 2)) + self.assertTrue(float_equal(self.lng, NORMALIZED_LAT_LNG_DISTANCE_STEP * 2)) + + def test_small_distance_same_spot(self): + sw = StepWalker(self.bot, 1, 0, 0) + self.assertEqual(sw.dLat, 0, 'dLat should be 0') + self.assertEqual(sw.dLng, 0, 'dLng should be 0') + + self.assertTrue(sw.step(), 'step should return True') + self.assertTrue(self.lat == self.bot.position[0]) + self.assertTrue(self.lng == self.bot.position[1]) + + def test_small_distance_small_step(self): + sw = StepWalker(self.bot, 1, 1e-5, 1e-5) + self.assertEqual(sw.dLat, 0) + self.assertEqual(sw.dLng, 0) + + @unittest.skip('This behavior is To Be Defined') + def test_big_distances(self): + # FIXME currently the StepWalker acts like it won't move if big distances gives as input + # see args below + # with self.assertRaises(RuntimeError): + sw = StepWalker(self.bot, 1, 10, 10) + sw.step() # equals True i.e act like the distance is too short for a step diff --git a/tests/tree_config_builder_test.py b/tests/tree_config_builder_test.py new file mode 100644 index 0000000000..cee1080280 --- /dev/null +++ b/tests/tree_config_builder_test.py @@ -0,0 +1,85 @@ +import unittest +import json +from pokemongo_bot import PokemonGoBot, ConfigException, TreeConfigBuilder +from pokemongo_bot.cell_workers import HandleSoftBan, CatchLuredPokemon + +def convert_from_json(str): + return json.loads(str) + +class TreeConfigBuilderTest(unittest.TestCase): + def setUp(self): + self.bot = {} + + def test_should_throw_on_no_type_key(self): + obj = convert_from_json("""[{ + "bad_key": "foo" + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + + self.assertRaisesRegexp( + ConfigException, + "No type found for given task", + builder.build) + + def test_should_throw_on_non_matching_type(self): + obj = convert_from_json("""[{ + "type": "foo" + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + + self.assertRaisesRegexp( + ConfigException, + "No worker named foo defined", + builder.build) + + def test_should_throw_on_wrong_evolve_task_name(self): + obj = convert_from_json("""[{ + "type": "EvolveAll" + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + + self.assertRaisesRegexp( + ConfigException, + "The EvolveAll task has been renamed to EvolvePokemon", + builder.build) + + def test_creating_worker(self): + obj = convert_from_json("""[{ + "type": "HandleSoftBan" + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + tree = builder.build() + + self.assertIsInstance(tree[0], HandleSoftBan) + self.assertIs(tree[0].bot, self.bot) + + def test_creating_two_workers(self): + obj = convert_from_json("""[{ + "type": "HandleSoftBan" + }, { + "type": "CatchLuredPokemon" + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + tree = builder.build() + + self.assertIsInstance(tree[0], HandleSoftBan) + self.assertIs(tree[0].bot, self.bot) + self.assertIsInstance(tree[1], CatchLuredPokemon) + self.assertIs(tree[1].bot, self.bot) + + def test_task_with_config(self): + obj = convert_from_json("""[{ + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + tree = builder.build() + self.assertTrue(tree[0].config.get('longer_eggs_first', False)) diff --git a/tests/update_title_stats_test.py b/tests/update_title_stats_test.py new file mode 100644 index 0000000000..699d736b7a --- /dev/null +++ b/tests/update_title_stats_test.py @@ -0,0 +1,131 @@ +import unittest +from datetime import datetime, timedelta +from mock import patch, MagicMock +from pokemongo_bot.cell_workers.update_title_stats import UpdateTitleStats +from tests import FakeBot + + +class UpdateTitleStatsTestCase(unittest.TestCase): + config = { + 'min_interval': 20, + 'stats': ['pokemon_evolved', 'pokemon_encountered', 'uptime', 'pokemon_caught', + 'stops_visited', 'km_walked', 'level', 'stardust_earned', 'level_completion', + 'xp_per_hour', 'pokeballs_thrown', 'highest_cp_pokemon', 'level_stats', + 'xp_earned', 'pokemon_unseen', 'most_perfect_pokemon', 'pokemon_stats', + 'pokemon_released'] + } + player_stats = { + 'level': 25, + 'prev_level_xp': 1250000, + 'next_level_xp': 1400000, + 'experience': 1337500 + } + + def setUp(self): + self.bot = FakeBot() + self.worker = UpdateTitleStats(self.bot, self.config) + + def mock_metrics(self): + self.bot.metrics = MagicMock() + self.bot.metrics.runtime.return_value = timedelta(hours=15, minutes=42, seconds=13) + self.bot.metrics.distance_travelled.return_value = 42.05 + self.bot.metrics.xp_per_hour.return_value = 1337.42 + self.bot.metrics.xp_earned.return_value = 424242 + self.bot.metrics.visits = {'latest': 250, 'start': 30} + self.bot.metrics.num_encounters.return_value = 130 + self.bot.metrics.num_captures.return_value = 120 + self.bot.metrics.releases = 30 + self.bot.metrics.num_evolutions.return_value = 12 + self.bot.metrics.num_new_mons.return_value = 3 + self.bot.metrics.num_throws.return_value = 145 + self.bot.metrics.earned_dust.return_value = 24069 + self.bot.metrics.highest_cp = {'desc': 'highest_cp'} + self.bot.metrics.most_perfect = {'desc': 'most_perfect'} + + def test_process_config(self): + self.assertEqual(self.worker.min_interval, self.config['min_interval']) + self.assertEqual(self.worker.displayed_stats, self.config['stats']) + + def test_should_display_no_next_update(self): + self.worker.next_update = None + + self.assertTrue(self.worker._should_display()) + + @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + def test_should_display_before_next_update(self, mock_datetime): + now = datetime.now() + mock_datetime.now.return_value = now - timedelta(seconds=20) + self.worker.next_update = now + + self.assertFalse(self.worker._should_display()) + + @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + def test_should_display_after_next_update(self, mock_datetime): + now = datetime.now() + mock_datetime.now.return_value = now + timedelta(seconds=20) + self.worker.next_update = now + + self.assertTrue(self.worker._should_display()) + + @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + def test_should_display_exactly_next_update(self, mock_datetime): + now = datetime.now() + mock_datetime.now.return_value = now + self.worker.next_update = now + + self.assertTrue(self.worker._should_display()) + + @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + def test_next_update_after_update_title(self, mock_datetime): + now = datetime.now() + mock_datetime.now.return_value = now + old_next_display_value = self.worker.next_update + self.worker._update_title('', 'linux2') + + self.assertNotEqual(self.worker.next_update, old_next_display_value) + self.assertEqual(self.worker.next_update, + now + timedelta(seconds=self.config['min_interval'])) + + @patch('pokemongo_bot.cell_workers.update_title_stats.stdout') + def test_update_title_linux_osx(self, mock_stdout): + self.worker._update_title('', 'linux') + + self.assertEqual(mock_stdout.write.call_count, 1) + + self.worker._update_title('', 'linux2') + + self.assertEqual(mock_stdout.write.call_count, 2) + + self.worker._update_title('', 'darwin') + + self.assertEqual(mock_stdout.write.call_count, 3) + + @unittest.skip("Didn't find a way to mock ctypes.windll.kernel32.SetConsoleTitleA") + def test_update_title_win32(self): + self.worker._update_title('', 'win32') + + def test_get_stats_title_player_stats_none(self): + title = self.worker._get_stats_title(None) + + self.assertEqual(title, '') + + def test_get_stats_no_displayed_stats(self): + self.worker.displayed_stats = [] + title = self.worker._get_stats_title(self.player_stats) + + self.assertEqual(title, '') + + def test_get_stats(self): + self.mock_metrics() + + title = self.worker._get_stats_title(self.player_stats) + expected = 'Evolved 12 pokemon | Encountered 130 pokemon | Uptime : 15:42:13 | ' \ + 'Caught 120 pokemon | Visited 220 stops | 42.05km walked | Level 25 | ' \ + 'Earned 24,069 Stardust | 87,500 / 150,000 XP (58%) | 1,337 XP/h | ' \ + 'Threw 145 pokeballs | Highest CP pokemon : highest_cp | ' \ + 'Level 25 (87,500 / 150,000, 58%) | +424,242 XP | ' \ + 'Encountered 3 new pokemon | Most perfect pokemon : most_perfect | ' \ + 'Encountered 130 pokemon, 120 caught, 30 released, 12 evolved, ' \ + '3 never seen before | Released 30 pokemon' + + self.assertEqual(title, expected) diff --git a/travis-pythoncheck.py b/travis-pythoncheck.py deleted file mode 100644 index 1095b04a12..0000000000 --- a/travis-pythoncheck.py +++ /dev/null @@ -1,48 +0,0 @@ -#! /usr/bin/env python -''' -Module that runs pylint on all python scripts found in a directory tree.. -''' - -import os -import re -import sys - -total = 0.0 -count = 0 - -def check(module): - ''' - apply pylint to the file specified if it is a *.py file - ''' - global total, count - - if module[-3:] == ".py": - - print "CHECKING ", module - pout = os.popen('pylint %s'% module, 'r') - for line in pout: - if re.match("E....:.", line): - print line - if "Your code has been rated at" in line: - print line - score = re.findall("\d.\d\d", line)[0] - total += float(score) - count += 1 - -if __name__ == "__main__": - try: - print sys.argv - BASE_DIRECTORY = sys.argv[1] - except IndexError: - print "no directory specified, defaulting to current working directory" - BASE_DIRECTORY = os.getcwd() - - print "looking for *.py scripts in subdirectories of ", BASE_DIRECTORY - for root, dirs, files in os.walk(BASE_DIRECTORY): - for name in files: - filepath = os.path.join(root, name) - check(filepath) - - print "==" * 50 - print "%d modules found"% count - print "AVERAGE SCORE = %.02f"% (total / count) \ No newline at end of file diff --git a/web b/web index dc742c598a..6ba5609c61 160000 --- a/web +++ b/web @@ -1 +1 @@ -Subproject commit dc742c598a2636337bd358dae8a558ef02159e8e +Subproject commit 6ba5609c6151507b5b832a74e471b6b7b1a182c9 diff --git a/ws_server.py b/ws_server.py new file mode 100755 index 0000000000..85d6bb3a01 --- /dev/null +++ b/ws_server.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# -*- coding: utf-8 - + +import argparse + +from pokemongo_bot.socketio_server.runner import SocketIoRunner + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument( + "--host", + help="Host for the websocket", + type=str, + default='localhost' + ) + parser.add_argument( + "--port", + help="Port for the websocket", + type=int, + default=4000 + ) + config = parser.parse_known_args()[0] + + s = SocketIoRunner("{}:{}".format(config.host, config.port)) + s._start_listening_blocking() From 38c2c6ccb5f3896bdb5ad5398e6a5dddf98e19b8 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sat, 6 Aug 2016 18:05:01 -0700 Subject: [PATCH 029/202] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 8fc2ed8741..dd23185df0 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,7 @@ PokemonGo bot is a project created by the [PokemonGoF](https://github.com/PokemonGoF) team. The project is currently setup in two main branches. `dev` and `master`. -## Niantic Changes - Niantic have changed API responses, meaning that this bot and anything accessing the API though POGOProtos is currently broken. A number of developers from /r/pokemondev are working to address this and come up with a fix for this issue, find the [current status here](https://www.reddit.com/r/pokemongodev/comments/4w1cvr/pokemongo_current_api_status/) +## we are currently working on the new branch [upgrade_to_the_new_api](https://github.com/PokemonGoF/PokemonGo-Bot/tree/upgrade_to_the_new_api) We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) From 55846108e235fda2eda370b99c9019f7d89e8619 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sat, 6 Aug 2016 20:19:05 -0700 Subject: [PATCH 030/202] Upgrade to the new api (#2636) * Upgraded to the latest api. * To fit the latest api changes. * Upgrade the pgoapi to commit fd462be412ac347ab517b68f269e2342a7226909 * Enable the exception, have a backup. for pokecli.py * Changed the GA beacon. * Revert "Enable the exception, have a backup. for pokecli.py" This reverts commit 60f8eb407a6588e79e141c764278b14cc6f7328a. * Bump pgoapi to c070a789029792a75be34aed408906ee3e3653f * Now should working. --- README.md | 2 +- pokemongo_bot/__init__.py | 2 +- pokemongo_bot/api_wrapper.py | 3 +-- requirements.txt | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index dd23185df0..49a2bbbc83 100644 --- a/README.md +++ b/README.md @@ -68,4 +68,4 @@ To ensure that all updates are documented - [@eggins](https://github.com/eggins) - [AHAAAAAAA](https://github.com/AHAAAAAAA/PokemonGo-Map) for parts of the s2sphere stuff -[![Analytics](https://ga-beacon.appspot.com/UA-81468120-1/welcome-page-master)](https://github.com/igrigorik/ga-beacon) +[![Analytics](https://ga-beacon.appspot.com/UA-81468120-1/welcome-page-new-api)](https://github.com/igrigorik/ga-beacon) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 045abd82e9..87910797a4 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -599,7 +599,7 @@ def _setup_api(self): # chain subrequests (methods) into one RPC call self._print_character_info() - + self.api.activate_signature("encrypt.so") self.logger.info('') self.update_inventory() # send empty map_cells and then our position diff --git a/pokemongo_bot/api_wrapper.py b/pokemongo_bot/api_wrapper.py index 324d645043..7224b0b4e4 100644 --- a/pokemongo_bot/api_wrapper.py +++ b/pokemongo_bot/api_wrapper.py @@ -21,8 +21,7 @@ def create_request(self): RequestClass = PGoApiRequest return RequestClass( - self._api_endpoint, - self._auth_provider, + self, self._position_lat, self._position_lng, self._position_alt diff --git a/requirements.txt b/requirements.txt index d8b5ce5c9f..5346ff7bc1 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@0811db23d639039f968a82e06c7aa15a0a5016b6#egg=pgoapi +-e git+https://github.com/keyphact/pgoapi.git@9c070a789029792a75be34aed408906ee3e3653f#egg=pgoapi geopy==1.11.0 protobuf==3.0.0b4 requests==2.10.0 From 0315892aa236c14ce6dfce8b777b2a6d755486eb Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sat, 6 Aug 2016 20:19:58 -0700 Subject: [PATCH 031/202] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 49a2bbbc83..5dac1a1497 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ PokemonGo bot is a project created by the [PokemonGoF](https://github.com/PokemonGoF) team. The project is currently setup in two main branches. `dev` and `master`. -## we are currently working on the new branch [upgrade_to_the_new_api](https://github.com/PokemonGoF/PokemonGo-Bot/tree/upgrade_to_the_new_api) +## Where to get the dll/so ? +You need grab them from internet. We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) From c21fad0e476a2ab77e8ec6eb898c03b1c3dbf3dd Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sat, 6 Aug 2016 20:27:51 -0700 Subject: [PATCH 032/202] Fixed a bug. --- pokemongo_bot/cell_workers/catch_visible_pokemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py index c9c6147c0a..1103815a4c 100644 --- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -16,7 +16,7 @@ def work(self): ) for pokemon in self.bot.cell['catchable_pokemons']: - with open(user_web_catchable, 'w') as outfile: + with open('user_web_catchable', 'w') as outfile: json.dump(pokemon, outfile) self.emit_event( 'catchable_pokemon', From c81666d751b9ca8cd338760fdf920194fabf4ee4 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sat, 6 Aug 2016 22:01:02 -0700 Subject: [PATCH 033/202] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5dac1a1497..ea02cdfe66 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -# PokemonGo-Bot +# PokemonGo-Bot (Working) PokemonGo bot is a project created by the [PokemonGoF](https://github.com/PokemonGoF) team. The project is currently setup in two main branches. `dev` and `master`. -## Where to get the dll/so ? +## Please submit PR to [Dev branch](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) + +## Where to get the dll/so ? A help channel is comming. You need grab them from internet. We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) From c58bed9d0f66cc093a097d5b2f611981c4602ee6 Mon Sep 17 00:00:00 2001 From: raulgbcr Date: Sun, 7 Aug 2016 07:06:30 +0200 Subject: [PATCH 034/202] Corrected "Spun" to "Spinning" (#2656) --- pokemongo_bot/cell_workers/spin_fort.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 9572008241..5f63523132 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -63,8 +63,8 @@ def work(self): if experience_awarded or items_awarded: self.emit_event( - 'spun_pokestop', - formatted="Spun pokestop {pokestop}. Experience awarded: {exp}. Items awarded: {items}", + 'spin_pokestop', + formatted="Spinning pokestop {pokestop}. Experience awarded: {exp}. Items awarded: {items}", data={ 'pokestop': fort_name, 'exp': experience_awarded, From dc0f3864f9a4f689d30e24c03b13807515a89669 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sat, 6 Aug 2016 22:15:38 -0700 Subject: [PATCH 035/202] Upgrade pgoapi to 249d3be7fbbdabc7f9adea17cbc899d6549e47a2. (#2658) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5346ff7bc1..aabf40937d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy==1.11.0 networkx==1.11 --e git+https://github.com/keyphact/pgoapi.git@9c070a789029792a75be34aed408906ee3e3653f#egg=pgoapi +-e git+https://github.com/keyphact/pgoapi.git@249d3be7fbbdabc7f9adea17cbc899d6549e47a2#egg=pgoapi geopy==1.11.0 protobuf==3.0.0b4 requests==2.10.0 From 97ab25a171b8de38a3412c7bdc68e24ac9b5d196 Mon Sep 17 00:00:00 2001 From: reddivision Date: Sat, 6 Aug 2016 22:49:06 -0700 Subject: [PATCH 036/202] fix for softban worker -- thanks a lot, doug (#2662) --- pokemongo_bot/cell_workers/handle_soft_ban.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pokemongo_bot/cell_workers/handle_soft_ban.py b/pokemongo_bot/cell_workers/handle_soft_ban.py index e266c5c377..4f9d416e83 100644 --- a/pokemongo_bot/cell_workers/handle_soft_ban.py +++ b/pokemongo_bot/cell_workers/handle_soft_ban.py @@ -47,21 +47,24 @@ def work(self): ) def spin_fort(self, fort): + fort_id = fort['id'] + latitude = fort['latitude'] + longitude = fort['longitude'] self.bot.api.fort_search( - fort_id=fort['id'], - fort_latitude=fort['latitude'], - fort_longitude=fort['longitude'], + fort_id=fort_id, + fort_latitude=latitude, + fort_longitude=longitude, player_latitude=f2i(self.bot.position[0]), player_longitude=f2i(self.bot.position[1]) ) - self.bot.event_handler.emit( + self.emit_event( 'spun_fort', level='debug', formatted="Spun fort {fort_id}", data={ 'fort_id': fort_id, - 'lat': fort['latitude'], - 'lng': fort['longitude'] + 'latitude': latitude, + 'longitude': longitude } ) From 44a6602ceecf202e5ab0bf05a960b8efcb595551 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sat, 6 Aug 2016 22:59:18 -0700 Subject: [PATCH 037/202] Revert "Corrected "Spun" to "Spinning"" (#2672) --- pokemongo_bot/cell_workers/spin_fort.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 5f63523132..9572008241 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -63,8 +63,8 @@ def work(self): if experience_awarded or items_awarded: self.emit_event( - 'spin_pokestop', - formatted="Spinning pokestop {pokestop}. Experience awarded: {exp}. Items awarded: {items}", + 'spun_pokestop', + formatted="Spun pokestop {pokestop}. Experience awarded: {exp}. Items awarded: {items}", data={ 'pokestop': fort_name, 'exp': experience_awarded, From ad6c21aa1dc0902c62e4001f9a81ce1fcd8a9f59 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sun, 7 Aug 2016 00:01:55 -0700 Subject: [PATCH 038/202] Update README.md (#2685) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1e56579ca8..ba131d5c39 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ To ensure that all updates are documented - [@eggins](https://github.com/eggins) ## Credits - [tejado](https://github.com/tejado) many thanks for the API +- [U6 Group](http://pgoapi.com) for the U6 - [Mila432](https://github.com/Mila432/Pokemon_Go_API) for the login secrets - [elliottcarlson](https://github.com/elliottcarlson) for the Google Auth PR - [AeonLucid](https://github.com/AeonLucid/POGOProtos) for improved protos From 85f7607c6fbc7e4b13dc370877f01f45c5e241af Mon Sep 17 00:00:00 2001 From: Eli White Date: Sun, 7 Aug 2016 00:14:06 -0700 Subject: [PATCH 039/202] Adding plugin support (#2679) * Adding plugin support * Adding an empty __init__.py --- pokecli.py | 6 ++++++ pokemongo_bot/__init__.py | 1 + pokemongo_bot/plugin_loader.py | 18 +++++++++++++++++ pokemongo_bot/test/plugin_loader_test.py | 20 +++++++++++++++++++ pokemongo_bot/test/resources/__init__.py | 0 .../test/resources/plugin_fixture/__init__.py | 1 + .../resources/plugin_fixture/fake_task.py | 5 +++++ pokemongo_bot/tree_config_builder.py | 11 +++++++++- tests/tree_config_builder_test.py | 18 ++++++++++++++++- 9 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 pokemongo_bot/plugin_loader.py create mode 100644 pokemongo_bot/test/plugin_loader_test.py create mode 100644 pokemongo_bot/test/resources/__init__.py create mode 100644 pokemongo_bot/test/resources/plugin_fixture/__init__.py create mode 100644 pokemongo_bot/test/resources/plugin_fixture/fake_task.py diff --git a/pokecli.py b/pokecli.py index 3d1b3736e7..c0023dc0db 100644 --- a/pokecli.py +++ b/pokecli.py @@ -40,6 +40,7 @@ from pokemongo_bot import PokemonGoBot, TreeConfigBuilder from pokemongo_bot.health_record import BotEvent +from pokemongo_bot.plugin_loader import PluginLoader if sys.version_info >= (2, 7, 9): ssl._create_default_https_context = ssl._create_unverified_context @@ -384,6 +385,7 @@ def init_config(): config.release = load.get('release', {}) config.action_wait_max = load.get('action_wait_max', 4) config.action_wait_min = load.get('action_wait_min', 1) + config.plugins = load.get('plugins', []) config.raw_tasks = load.get('tasks', []) config.vips = load.get('vips', {}) @@ -439,6 +441,10 @@ def task_configuration_error(flag_name): parser.error("--catch_randomize_spin_factor is out of range! (should be 0 <= catch_randomize_spin_factor <= 1)") return None + plugin_loader = PluginLoader() + for plugin in config.plugins: + plugin_loader.load_path(plugin) + # create web dir if not exists try: os.makedirs(web_dir) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 1d318f473b..7883b16bf1 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -15,6 +15,7 @@ from pgoapi.utilities import f2i, get_cell_ids import cell_workers +from plugin_loader import PluginLoader from api_wrapper import ApiWrapper from cell_workers.utils import distance from event_manager import EventManager diff --git a/pokemongo_bot/plugin_loader.py b/pokemongo_bot/plugin_loader.py new file mode 100644 index 0000000000..f3c1fd9c2f --- /dev/null +++ b/pokemongo_bot/plugin_loader.py @@ -0,0 +1,18 @@ +import os +import sys +import importlib + +class PluginLoader(object): + folder_cache = [] + + def load_path(self, path): + parent_dir = os.path.dirname(path) + if parent_dir not in self.folder_cache: + self.folder_cache.append(parent_dir) + sys.path.append(parent_dir) + + def get_class(self, namespace_class): + [namespace, class_name] = namespace_class.split('.') + my_module = importlib.import_module(namespace) + return getattr(my_module, class_name) + diff --git a/pokemongo_bot/test/plugin_loader_test.py b/pokemongo_bot/test/plugin_loader_test.py new file mode 100644 index 0000000000..2960c5d36b --- /dev/null +++ b/pokemongo_bot/test/plugin_loader_test.py @@ -0,0 +1,20 @@ +import imp +import sys +import pkgutil +import importlib +import unittest +import os +from datetime import timedelta, datetime +from mock import patch, MagicMock +from pokemongo_bot.plugin_loader import PluginLoader +from pokemongo_bot.test.resources.plugin_fixture import FakeTask + +class PluginLoaderTest(unittest.TestCase): + def setUp(self): + self.plugin_loader = PluginLoader() + + def test_load_namespace_class(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') + self.plugin_loader.load_path(package_path) + loaded_class = self.plugin_loader.get_class('plugin_fixture.FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') diff --git a/pokemongo_bot/test/resources/__init__.py b/pokemongo_bot/test/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pokemongo_bot/test/resources/plugin_fixture/__init__.py b/pokemongo_bot/test/resources/plugin_fixture/__init__.py new file mode 100644 index 0000000000..647158bf44 --- /dev/null +++ b/pokemongo_bot/test/resources/plugin_fixture/__init__.py @@ -0,0 +1 @@ +from fake_task import FakeTask diff --git a/pokemongo_bot/test/resources/plugin_fixture/fake_task.py b/pokemongo_bot/test/resources/plugin_fixture/fake_task.py new file mode 100644 index 0000000000..f8d95e9a50 --- /dev/null +++ b/pokemongo_bot/test/resources/plugin_fixture/fake_task.py @@ -0,0 +1,5 @@ +from pokemongo_bot.cell_workers import BaseTask + +class FakeTask(BaseTask): + def work(self): + return 'FakeTask' diff --git a/pokemongo_bot/tree_config_builder.py b/pokemongo_bot/tree_config_builder.py index bba63ad880..ce62c7fc61 100644 --- a/pokemongo_bot/tree_config_builder.py +++ b/pokemongo_bot/tree_config_builder.py @@ -1,4 +1,5 @@ import cell_workers +from pokemongo_bot.plugin_loader import PluginLoader class ConfigException(Exception): pass @@ -7,6 +8,7 @@ class TreeConfigBuilder(object): def __init__(self, bot, tasks_raw): self.bot = bot self.tasks_raw = tasks_raw + self.plugin_loader = PluginLoader() def _get_worker_by_name(self, name): try: @@ -16,6 +18,9 @@ def _get_worker_by_name(self, name): return worker + def _is_plugin_task(self, name): + return '.' in name + def build(self): workers = [] @@ -28,7 +33,11 @@ def build(self): task_config = task.get('config', {}) - worker = self._get_worker_by_name(task_type) + if self._is_plugin_task(task_type): + worker = self.plugin_loader.get_class(task_type) + else: + worker = self._get_worker_by_name(task_type) + instance = worker(self.bot, task_config) workers.append(instance) diff --git a/tests/tree_config_builder_test.py b/tests/tree_config_builder_test.py index cee1080280..fd1ca0d00e 100644 --- a/tests/tree_config_builder_test.py +++ b/tests/tree_config_builder_test.py @@ -1,7 +1,9 @@ import unittest import json -from pokemongo_bot import PokemonGoBot, ConfigException, TreeConfigBuilder +import os +from pokemongo_bot import PokemonGoBot, ConfigException, TreeConfigBuilder, PluginLoader from pokemongo_bot.cell_workers import HandleSoftBan, CatchLuredPokemon +from pokemongo_bot.test.resources.plugin_fixture import FakeTask def convert_from_json(str): return json.loads(str) @@ -83,3 +85,17 @@ def test_task_with_config(self): builder = TreeConfigBuilder(self.bot, obj) tree = builder.build() self.assertTrue(tree[0].config.get('longer_eggs_first', False)) + + def test_load_plugin_task(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') + plugin_loader = PluginLoader() + plugin_loader.load_path(package_path) + + obj = convert_from_json("""[{ + "type": "plugin_fixture.FakeTask" + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + tree = builder.build() + result = tree[0].work() + self.assertEqual(result, 'FakeTask') From f114be660fb90677b01fed03fc6585f4f2b0818d Mon Sep 17 00:00:00 2001 From: Eli White Date: Sun, 7 Aug 2016 01:27:16 -0700 Subject: [PATCH 040/202] Moving the base task to the project root (#2702) * Moving the base task to the project root * Moving the base class more * Changing the import again --- pokemongo_bot/__init__.py | 1 + pokemongo_bot/{cell_workers => }/base_task.py | 0 pokemongo_bot/cell_workers/__init__.py | 3 +-- pokemongo_bot/cell_workers/catch_lured_pokemon.py | 2 +- pokemongo_bot/cell_workers/catch_visible_pokemon.py | 2 +- pokemongo_bot/cell_workers/collect_level_up_reward.py | 2 +- pokemongo_bot/cell_workers/evolve_pokemon.py | 2 +- pokemongo_bot/cell_workers/follow_cluster.py | 2 +- pokemongo_bot/cell_workers/follow_path.py | 2 +- pokemongo_bot/cell_workers/follow_spiral.py | 2 +- pokemongo_bot/cell_workers/handle_soft_ban.py | 2 +- pokemongo_bot/cell_workers/incubate_eggs.py | 2 +- pokemongo_bot/cell_workers/move_to_fort.py | 2 +- pokemongo_bot/cell_workers/move_to_map_pokemon.py | 2 +- pokemongo_bot/cell_workers/nickname_pokemon.py | 2 +- pokemongo_bot/cell_workers/pokemon_catch_worker.py | 2 +- pokemongo_bot/cell_workers/recycle_items.py | 2 +- pokemongo_bot/cell_workers/sleep_schedule.py | 2 +- pokemongo_bot/cell_workers/spin_fort.py | 2 +- pokemongo_bot/cell_workers/transfer_pokemon.py | 2 +- pokemongo_bot/cell_workers/update_title_stats.py | 2 +- pokemongo_bot/test/resources/plugin_fixture/fake_task.py | 2 +- tests/base_task_test.py | 2 +- 23 files changed, 22 insertions(+), 22 deletions(-) rename pokemongo_bot/{cell_workers => }/base_task.py (100%) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 7883b16bf1..725e051c03 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -15,6 +15,7 @@ from pgoapi.utilities import f2i, get_cell_ids import cell_workers +from base_task import BaseTask from plugin_loader import PluginLoader from api_wrapper import ApiWrapper from cell_workers.utils import distance diff --git a/pokemongo_bot/cell_workers/base_task.py b/pokemongo_bot/base_task.py similarity index 100% rename from pokemongo_bot/cell_workers/base_task.py rename to pokemongo_bot/base_task.py diff --git a/pokemongo_bot/cell_workers/__init__.py b/pokemongo_bot/cell_workers/__init__.py index bc6638d1fd..68d181947a 100644 --- a/pokemongo_bot/cell_workers/__init__.py +++ b/pokemongo_bot/cell_workers/__init__.py @@ -15,7 +15,6 @@ from follow_path import FollowPath from follow_spiral import FollowSpiral from collect_level_up_reward import CollectLevelUpReward -from base_task import BaseTask from follow_cluster import FollowCluster from sleep_schedule import SleepSchedule -from update_title_stats import UpdateTitleStats \ No newline at end of file +from update_title_stats import UpdateTitleStats diff --git a/pokemongo_bot/cell_workers/catch_lured_pokemon.py b/pokemongo_bot/cell_workers/catch_lured_pokemon.py index bf2d45bb4b..8da2eae36a 100644 --- a/pokemongo_bot/cell_workers/catch_lured_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_lured_pokemon.py @@ -3,7 +3,7 @@ from pokemongo_bot.cell_workers.utils import fort_details from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class CatchLuredPokemon(BaseTask): diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py index 1103815a4c..6ddb5a06d0 100644 --- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -1,6 +1,6 @@ import json -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker from utils import distance diff --git a/pokemongo_bot/cell_workers/collect_level_up_reward.py b/pokemongo_bot/cell_workers/collect_level_up_reward.py index 304818fe2b..fd74acf6eb 100644 --- a/pokemongo_bot/cell_workers/collect_level_up_reward.py +++ b/pokemongo_bot/cell_workers/collect_level_up_reward.py @@ -1,4 +1,4 @@ -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class CollectLevelUpReward(BaseTask): diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 911d6a1f67..1f1a19e1e3 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -1,6 +1,6 @@ from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.item_list import Item -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class EvolvePokemon(BaseTask): diff --git a/pokemongo_bot/cell_workers/follow_cluster.py b/pokemongo_bot/cell_workers/follow_cluster.py index 02d3880a7e..26fbd6ace9 100644 --- a/pokemongo_bot/cell_workers/follow_cluster.py +++ b/pokemongo_bot/cell_workers/follow_cluster.py @@ -1,7 +1,7 @@ from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.cell_workers.utils import distance from pokemongo_bot.cell_workers.utils import find_biggest_cluster -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class FollowCluster(BaseTask): diff --git a/pokemongo_bot/cell_workers/follow_path.py b/pokemongo_bot/cell_workers/follow_path.py index 04eb817593..7219bb98a6 100644 --- a/pokemongo_bot/cell_workers/follow_path.py +++ b/pokemongo_bot/cell_workers/follow_path.py @@ -3,7 +3,7 @@ import gpxpy import gpxpy.gpx import json -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.utils import distance, i2f, format_dist from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.step_walker import StepWalker diff --git a/pokemongo_bot/cell_workers/follow_spiral.py b/pokemongo_bot/cell_workers/follow_spiral.py index 28b548d1ca..9b7781aeca 100644 --- a/pokemongo_bot/cell_workers/follow_spiral.py +++ b/pokemongo_bot/cell_workers/follow_spiral.py @@ -5,7 +5,7 @@ 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 +from pokemongo_bot.base_task import BaseTask class FollowSpiral(BaseTask): def initialize(self): diff --git a/pokemongo_bot/cell_workers/handle_soft_ban.py b/pokemongo_bot/cell_workers/handle_soft_ban.py index 4f9d416e83..d00679d2ec 100644 --- a/pokemongo_bot/cell_workers/handle_soft_ban.py +++ b/pokemongo_bot/cell_workers/handle_soft_ban.py @@ -3,7 +3,7 @@ from pgoapi.utilities import f2i from pokemongo_bot.constants import Constants -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers import MoveToFort from pokemongo_bot.cell_workers.utils import distance from pokemongo_bot.worker_result import WorkerResult diff --git a/pokemongo_bot/cell_workers/incubate_eggs.py b/pokemongo_bot/cell_workers/incubate_eggs.py index 9e21b0d280..c49a7f07e2 100644 --- a/pokemongo_bot/cell_workers/incubate_eggs.py +++ b/pokemongo_bot/cell_workers/incubate_eggs.py @@ -1,5 +1,5 @@ from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class IncubateEggs(BaseTask): diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index f43d1641e6..25d4e91490 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -4,7 +4,7 @@ from pokemongo_bot.constants import Constants from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from utils import distance, format_dist, fort_details diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index bce39e0143..7a7dfb9f38 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -9,7 +9,7 @@ from pokemongo_bot.cell_workers.utils import distance, format_dist, format_time from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker diff --git a/pokemongo_bot/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py index 29df15ae4a..f344ba1ac5 100644 --- a/pokemongo_bot/cell_workers/nickname_pokemon.py +++ b/pokemongo_bot/cell_workers/nickname_pokemon.py @@ -1,5 +1,5 @@ from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class NicknamePokemon(BaseTask): def initialize(self): diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index afa5578267..d676e9f0e2 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -3,7 +3,7 @@ import time from pokemongo_bot.human_behaviour import (normalized_reticle_size, sleep, spin_modifier) -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class PokemonCatchWorker(BaseTask): BAG_FULL = 'bag_full' diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index c28b2749b1..7c58111d69 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -1,6 +1,6 @@ import json import os -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.tree_config_builder import ConfigException class RecycleItems(BaseTask): diff --git a/pokemongo_bot/cell_workers/sleep_schedule.py b/pokemongo_bot/cell_workers/sleep_schedule.py index daaf0b8f1e..81d1642d9b 100644 --- a/pokemongo_bot/cell_workers/sleep_schedule.py +++ b/pokemongo_bot/cell_workers/sleep_schedule.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta from time import sleep from random import uniform -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class SleepSchedule(BaseTask): diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 9572008241..2bd8b49c87 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -8,7 +8,7 @@ from pokemongo_bot.constants import Constants from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.worker_result import WorkerResult -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from utils import distance, format_time, fort_details diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 70c5939c58..f815768558 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -1,7 +1,7 @@ import json from pokemongo_bot.human_behaviour import action_delay -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class TransferPokemon(BaseTask): diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index 911d2efd4d..13b80fc11a 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -2,7 +2,7 @@ from sys import stdout, platform as _platform from datetime import datetime, timedelta -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.worker_result import WorkerResult from pokemongo_bot.tree_config_builder import ConfigException diff --git a/pokemongo_bot/test/resources/plugin_fixture/fake_task.py b/pokemongo_bot/test/resources/plugin_fixture/fake_task.py index f8d95e9a50..0965b1ffe6 100644 --- a/pokemongo_bot/test/resources/plugin_fixture/fake_task.py +++ b/pokemongo_bot/test/resources/plugin_fixture/fake_task.py @@ -1,4 +1,4 @@ -from pokemongo_bot.cell_workers import BaseTask +from pokemongo_bot.base_task import BaseTask class FakeTask(BaseTask): def work(self): diff --git a/tests/base_task_test.py b/tests/base_task_test.py index 16684d900c..ee259f80dc 100644 --- a/tests/base_task_test.py +++ b/tests/base_task_test.py @@ -1,6 +1,6 @@ import unittest import json -from pokemongo_bot.cell_workers import BaseTask +from pokemongo_bot.base_task import BaseTask class FakeTask(BaseTask): def initialize(self): From 0b319bc243a1b11b17c8639f8d902b41ec233e6c Mon Sep 17 00:00:00 2001 From: Eli White Date: Sun, 7 Aug 2016 01:48:18 -0700 Subject: [PATCH 041/202] Adding a heartbeat to the analytics (#2709) * Adding a heartbeat to the analytics * Heartbeat every 30 seconds, not every 5 --- pokecli.py | 1 + pokemongo_bot/__init__.py | 1 + pokemongo_bot/health_record/bot_event.py | 10 ++++++++++ 3 files changed, 12 insertions(+) diff --git a/pokecli.py b/pokecli.py index c0023dc0db..f3b5a0416b 100644 --- a/pokecli.py +++ b/pokecli.py @@ -74,6 +74,7 @@ def main(): tree = TreeConfigBuilder(bot, config.raw_tasks).build() bot.workers = tree bot.metrics.capture_stats() + bot.health_record = health_record bot.event_manager.emit( 'bot_start', diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 725e051c03..8fa4e8247a 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -385,6 +385,7 @@ def _register_events(self): self.event_manager.register_event('unset_pokemon_nickname') def tick(self): + self.health_record.heartbeat() self.cell = self.get_meta_cell() self.tick_count += 1 diff --git a/pokemongo_bot/health_record/bot_event.py b/pokemongo_bot/health_record/bot_event.py index 986b5f3c70..ddcd0871e3 100644 --- a/pokemongo_bot/health_record/bot_event.py +++ b/pokemongo_bot/health_record/bot_event.py @@ -7,6 +7,7 @@ import os import uuid import requests +import time class BotEvent(object): def __init__(self, config): @@ -30,6 +31,8 @@ def __init__(self, config): logging = False, context = {} ) + self.heartbeat_wait = 30 # seconds + self.last_heartbeat = time.time() def capture_error(self): if self.config.health_record: @@ -37,6 +40,7 @@ def capture_error(self): def login_success(self): if self.config.health_record: + self.last_heartbeat = time.time() track_url('/loggedin') def login_failed(self): @@ -51,6 +55,12 @@ def logout(self): if self.config.health_record: track_url('/logout') + def heartbeat(self): + if self.config.health_record: + current_time = time.time() + if current_time - self.heartbeat_wait > self.last_heartbeat: + self.last_heartbeat = current_time + track_url('/heartbeat') def track_url(path): data = { From 1c369a408fd13afb33c8610e1c47efa9a725b978 Mon Sep 17 00:00:00 2001 From: Eli White Date: Sun, 7 Aug 2016 02:38:33 -0700 Subject: [PATCH 042/202] Don't double track clients --- pokemongo_bot/health_record/bot_event.py | 36 +++++++++++++----------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/pokemongo_bot/health_record/bot_event.py b/pokemongo_bot/health_record/bot_event.py index ddcd0871e3..36c323f791 100644 --- a/pokemongo_bot/health_record/bot_event.py +++ b/pokemongo_bot/health_record/bot_event.py @@ -31,6 +31,8 @@ def __init__(self, config): logging = False, context = {} ) + + self.client_id = uuid.uuid4() self.heartbeat_wait = 30 # seconds self.last_heartbeat = time.time() @@ -41,38 +43,38 @@ def capture_error(self): def login_success(self): if self.config.health_record: self.last_heartbeat = time.time() - track_url('/loggedin') + self.track_url('/loggedin') def login_failed(self): if self.config.health_record: - track_url('/login') + self.track_url('/login') def login_retry(self): if self.config.health_record: - track_url('/relogin') + self.track_url('/relogin') def logout(self): if self.config.health_record: - track_url('/logout') + self.track_url('/logout') def heartbeat(self): if self.config.health_record: current_time = time.time() if current_time - self.heartbeat_wait > self.last_heartbeat: self.last_heartbeat = current_time - track_url('/heartbeat') + self.track_url('/heartbeat') -def track_url(path): - data = { - 'v': '1', - 'tid': 'UA-81469507-1', - 'aip': '1', # Anonymize IPs - 'cid': uuid.uuid4(), - 't': 'pageview', - 'dp': path - } + def track_url(self, path): + data = { + 'v': '1', + 'tid': 'UA-81469507-1', + 'aip': '1', # Anonymize IPs + 'cid': self.client_id, + 't': 'pageview', + 'dp': path + } - response = requests.post( - 'http://www.google-analytics.com/collect', data=data) + response = requests.post( + 'http://www.google-analytics.com/collect', data=data) - response.raise_for_status() + response.raise_for_status() From e16b5ea17563b4af58ef4b49edb0c24489b6106d Mon Sep 17 00:00:00 2001 From: Eli White Date: Sun, 7 Aug 2016 02:49:25 -0700 Subject: [PATCH 043/202] Fix 'local variable 'bot' referenced before assignment' --- pokecli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pokecli.py b/pokecli.py index f3b5a0416b..38f33a035c 100644 --- a/pokecli.py +++ b/pokecli.py @@ -53,6 +53,7 @@ def main(): try: + bot = False logger.info('PokemonGO Bot v1.0') sys.stdout = codecs.getwriter('utf8')(sys.stdout) sys.stderr = codecs.getwriter('utf8')(sys.stderr) From e93431c3492e3c44414a540a746e6ba570865564 Mon Sep 17 00:00:00 2001 From: Eli White Date: Sun, 7 Aug 2016 02:57:33 -0700 Subject: [PATCH 044/202] Providing an error if tasks don't work for the given api (#2732) --- pokemongo_bot/__init__.py | 2 +- pokemongo_bot/base_task.py | 1 + .../cell_workers/catch_lured_pokemon.py | 2 + .../cell_workers/catch_visible_pokemon.py | 2 + .../cell_workers/collect_level_up_reward.py | 2 + pokemongo_bot/cell_workers/evolve_pokemon.py | 1 + pokemongo_bot/cell_workers/follow_cluster.py | 1 + pokemongo_bot/cell_workers/follow_path.py | 2 + pokemongo_bot/cell_workers/follow_spiral.py | 2 + pokemongo_bot/cell_workers/handle_soft_ban.py | 2 + pokemongo_bot/cell_workers/incubate_eggs.py | 2 + pokemongo_bot/cell_workers/move_to_fort.py | 1 + .../cell_workers/move_to_map_pokemon.py | 2 + .../cell_workers/nickname_pokemon.py | 2 + pokemongo_bot/cell_workers/recycle_items.py | 2 + pokemongo_bot/cell_workers/sleep_schedule.py | 1 + pokemongo_bot/cell_workers/spin_fort.py | 2 + .../cell_workers/transfer_pokemon.py | 2 + .../cell_workers/update_title_stats.py | 1 + .../test/resources/plugin_fixture/__init__.py | 1 + .../resources/plugin_fixture/fake_task.py | 2 + .../plugin_fixture/unsupported_api_task.py | 7 ++++ pokemongo_bot/tree_config_builder.py | 22 ++++++++++ tests/tree_config_builder_test.py | 41 ++++++++++++++++++- 24 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 pokemongo_bot/test/resources/plugin_fixture/unsupported_api_task.py diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 8fa4e8247a..c31c1085b3 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -27,7 +27,7 @@ from pokemongo_bot.socketio_server.runner import SocketIoRunner from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl from worker_result import WorkerResult -from tree_config_builder import ConfigException, TreeConfigBuilder +from tree_config_builder import ConfigException, MismatchTaskApiVersion, TreeConfigBuilder class PokemonGoBot(object): diff --git a/pokemongo_bot/base_task.py b/pokemongo_bot/base_task.py index ac48b9a676..22bbedf4e8 100644 --- a/pokemongo_bot/base_task.py +++ b/pokemongo_bot/base_task.py @@ -2,6 +2,7 @@ class BaseTask(object): + TASK_API_VERSION = 1 def __init__(self, bot, config): self.bot = bot diff --git a/pokemongo_bot/cell_workers/catch_lured_pokemon.py b/pokemongo_bot/cell_workers/catch_lured_pokemon.py index 8da2eae36a..c0e9283b82 100644 --- a/pokemongo_bot/cell_workers/catch_lured_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_lured_pokemon.py @@ -7,6 +7,8 @@ class CatchLuredPokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def work(self): lured_pokemon = self.get_lured_pokemon() if lured_pokemon: diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py index 6ddb5a06d0..102d6c01c8 100644 --- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -6,6 +6,8 @@ class CatchVisiblePokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def work(self): if 'catchable_pokemons' in self.bot.cell and len(self.bot.cell['catchable_pokemons']) > 0: # Sort all by distance from current pos- eventually this should diff --git a/pokemongo_bot/cell_workers/collect_level_up_reward.py b/pokemongo_bot/cell_workers/collect_level_up_reward.py index fd74acf6eb..950f450660 100644 --- a/pokemongo_bot/cell_workers/collect_level_up_reward.py +++ b/pokemongo_bot/cell_workers/collect_level_up_reward.py @@ -2,6 +2,8 @@ class CollectLevelUpReward(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + current_level = 0 previous_level = 0 diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 1f1a19e1e3..74eb0abf79 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -4,6 +4,7 @@ class EvolvePokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 def initialize(self): self.api = self.bot.api diff --git a/pokemongo_bot/cell_workers/follow_cluster.py b/pokemongo_bot/cell_workers/follow_cluster.py index 26fbd6ace9..8448fcf742 100644 --- a/pokemongo_bot/cell_workers/follow_cluster.py +++ b/pokemongo_bot/cell_workers/follow_cluster.py @@ -4,6 +4,7 @@ from pokemongo_bot.base_task import BaseTask class FollowCluster(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 def initialize(self): self.is_at_destination = False diff --git a/pokemongo_bot/cell_workers/follow_path.py b/pokemongo_bot/cell_workers/follow_path.py index 7219bb98a6..6e183ed1d7 100644 --- a/pokemongo_bot/cell_workers/follow_path.py +++ b/pokemongo_bot/cell_workers/follow_path.py @@ -11,6 +11,8 @@ class FollowPath(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): self.ptr = 0 self._process_config() diff --git a/pokemongo_bot/cell_workers/follow_spiral.py b/pokemongo_bot/cell_workers/follow_spiral.py index 9b7781aeca..f175369e45 100644 --- a/pokemongo_bot/cell_workers/follow_spiral.py +++ b/pokemongo_bot/cell_workers/follow_spiral.py @@ -8,6 +8,8 @@ from pokemongo_bot.base_task import BaseTask class FollowSpiral(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): self.steplimit = self.config.get("diameter", 4) self.step_size = self.config.get("step_size", 70) diff --git a/pokemongo_bot/cell_workers/handle_soft_ban.py b/pokemongo_bot/cell_workers/handle_soft_ban.py index d00679d2ec..8018b7c33e 100644 --- a/pokemongo_bot/cell_workers/handle_soft_ban.py +++ b/pokemongo_bot/cell_workers/handle_soft_ban.py @@ -10,6 +10,8 @@ class HandleSoftBan(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def work(self): if not self.should_run(): return diff --git a/pokemongo_bot/cell_workers/incubate_eggs.py b/pokemongo_bot/cell_workers/incubate_eggs.py index c49a7f07e2..5761090ea5 100644 --- a/pokemongo_bot/cell_workers/incubate_eggs.py +++ b/pokemongo_bot/cell_workers/incubate_eggs.py @@ -3,6 +3,8 @@ class IncubateEggs(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + last_km_walked = 0 def initialize(self): diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 25d4e91490..e4b4187d20 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -9,6 +9,7 @@ class MoveToFort(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 def initialize(self): self.lure_distance = 0 diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 7a7dfb9f38..08ff35f281 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -14,6 +14,8 @@ class MoveToMapPokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): self.last_map_update = 0 self.pokemon_data = self.bot.pokemon_list diff --git a/pokemongo_bot/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py index f344ba1ac5..cda206ad20 100644 --- a/pokemongo_bot/cell_workers/nickname_pokemon.py +++ b/pokemongo_bot/cell_workers/nickname_pokemon.py @@ -2,6 +2,8 @@ from pokemongo_bot.base_task import BaseTask class NicknamePokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): self.template = self.config.get('nickname_template','').lower().strip() if self.template == "{name}": diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 7c58111d69..2c969913b0 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -4,6 +4,8 @@ from pokemongo_bot.tree_config_builder import ConfigException class RecycleItems(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): self.item_filter = self.config.get('item_filter', {}) self._validate_item_filter() diff --git a/pokemongo_bot/cell_workers/sleep_schedule.py b/pokemongo_bot/cell_workers/sleep_schedule.py index 81d1642d9b..5a7d617e23 100644 --- a/pokemongo_bot/cell_workers/sleep_schedule.py +++ b/pokemongo_bot/cell_workers/sleep_schedule.py @@ -27,6 +27,7 @@ class SleepSchedule(BaseTask): duration_random_offset: (HH:MM) random offset of duration of sleep for this example the possible duration is 5:00-6:00 """ + SUPPORTED_TASK_API_VERSION = 1 LOG_INTERVAL_SECONDS = 600 SCHEDULING_MARGIN = timedelta(minutes=10) # Skip if next sleep is RESCHEDULING_MARGIN from now diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 2bd8b49c87..e04a86dbc6 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -13,6 +13,8 @@ class SpinFort(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def should_run(self): if not self.bot.has_space_for_loot(): self.emit_event( diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index f815768558..c48e17b20d 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -5,6 +5,8 @@ class TransferPokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def work(self): pokemon_groups = self._release_pokemon_get_groups() for pokemon_id in pokemon_groups: diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index 13b80fc11a..1f9b2a0e9b 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -49,6 +49,7 @@ class UpdateTitleStats(BaseTask): stats : An array of stats to display and their display order (implicitly), see available stats above. """ + SUPPORTED_TASK_API_VERSION = 1 DEFAULT_MIN_INTERVAL = 10 DEFAULT_DISPLAYED_STATS = [] diff --git a/pokemongo_bot/test/resources/plugin_fixture/__init__.py b/pokemongo_bot/test/resources/plugin_fixture/__init__.py index 647158bf44..57caf83dce 100644 --- a/pokemongo_bot/test/resources/plugin_fixture/__init__.py +++ b/pokemongo_bot/test/resources/plugin_fixture/__init__.py @@ -1 +1,2 @@ from fake_task import FakeTask +from unsupported_api_task import UnsupportedApiTask diff --git a/pokemongo_bot/test/resources/plugin_fixture/fake_task.py b/pokemongo_bot/test/resources/plugin_fixture/fake_task.py index 0965b1ffe6..ff729adee4 100644 --- a/pokemongo_bot/test/resources/plugin_fixture/fake_task.py +++ b/pokemongo_bot/test/resources/plugin_fixture/fake_task.py @@ -1,5 +1,7 @@ from pokemongo_bot.base_task import BaseTask class FakeTask(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def work(self): return 'FakeTask' diff --git a/pokemongo_bot/test/resources/plugin_fixture/unsupported_api_task.py b/pokemongo_bot/test/resources/plugin_fixture/unsupported_api_task.py new file mode 100644 index 0000000000..871e38f82d --- /dev/null +++ b/pokemongo_bot/test/resources/plugin_fixture/unsupported_api_task.py @@ -0,0 +1,7 @@ +from pokemongo_bot.base_task import BaseTask + +class UnsupportedApiTask(BaseTask): + SUPPORTED_TASK_API_VERSION = 2 + + def work(): + return 2 diff --git a/pokemongo_bot/tree_config_builder.py b/pokemongo_bot/tree_config_builder.py index ce62c7fc61..6242747b25 100644 --- a/pokemongo_bot/tree_config_builder.py +++ b/pokemongo_bot/tree_config_builder.py @@ -1,9 +1,13 @@ import cell_workers from pokemongo_bot.plugin_loader import PluginLoader +from pokemongo_bot.base_task import BaseTask class ConfigException(Exception): pass +class MismatchTaskApiVersion(Exception): + pass + class TreeConfigBuilder(object): def __init__(self, bot, tasks_raw): self.bot = bot @@ -38,6 +42,24 @@ def build(self): else: worker = self._get_worker_by_name(task_type) + error_string = '' + if BaseTask.TASK_API_VERSION < worker.SUPPORTED_TASK_API_VERSION: + error_string = 'Do you need to update the bot?' + + elif BaseTask.TASK_API_VERSION > worker.SUPPORTED_TASK_API_VERSION: + error_string = 'Is there a new version of this task?' + + if error_string != '': + raise MismatchTaskApiVersion( + 'Task {} only works with task api version {}, you are currently running version {}. {}' + .format( + task_type, + worker.SUPPORTED_TASK_API_VERSION, + BaseTask.TASK_API_VERSION, + error_string + ) + ) + instance = worker(self.bot, task_config) workers.append(instance) diff --git a/tests/tree_config_builder_test.py b/tests/tree_config_builder_test.py index fd1ca0d00e..cd8bed22e0 100644 --- a/tests/tree_config_builder_test.py +++ b/tests/tree_config_builder_test.py @@ -1,9 +1,9 @@ import unittest import json import os -from pokemongo_bot import PokemonGoBot, ConfigException, TreeConfigBuilder, PluginLoader +from pokemongo_bot import PokemonGoBot, ConfigException, MismatchTaskApiVersion, TreeConfigBuilder, PluginLoader, BaseTask from pokemongo_bot.cell_workers import HandleSoftBan, CatchLuredPokemon -from pokemongo_bot.test.resources.plugin_fixture import FakeTask +from pokemongo_bot.test.resources.plugin_fixture import FakeTask, UnsupportedApiTask def convert_from_json(str): return json.loads(str) @@ -99,3 +99,40 @@ def test_load_plugin_task(self): tree = builder.build() result = tree[0].work() self.assertEqual(result, 'FakeTask') + + def setupUnsupportedBuilder(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'pokemongo_bot', 'test', 'resources', 'plugin_fixture') + plugin_loader = PluginLoader() + plugin_loader.load_path(package_path) + + obj = convert_from_json("""[{ + "type": "plugin_fixture.UnsupportedApiTask" + }]""") + + return TreeConfigBuilder(self.bot, obj) + + def test_task_version_too_high(self): + builder = self.setupUnsupportedBuilder() + + previous_version = BaseTask.TASK_API_VERSION + BaseTask.TASK_API_VERSION = 1 + + self.assertRaisesRegexp( + MismatchTaskApiVersion, + "Task plugin_fixture.UnsupportedApiTask only works with task api version 2, you are currently running version 1. Do you need to update the bot?", + builder.build) + + BaseTask.TASK_API_VERSION = previous_version + + def test_task_version_too_low(self): + builder = self.setupUnsupportedBuilder() + + previous_version = BaseTask.TASK_API_VERSION + BaseTask.TASK_API_VERSION = 3 + + self.assertRaisesRegexp( + MismatchTaskApiVersion, + "Task plugin_fixture.UnsupportedApiTask only works with task api version 2, you are currently running version 3. Is there a new version of this task?", + builder.build) + + BaseTask.TASK_API_VERSION = previous_version From d5bb09fd758e779b8eee859d8374f0d198e58abb Mon Sep 17 00:00:00 2001 From: David Kim Date: Sun, 7 Aug 2016 06:32:43 -0400 Subject: [PATCH 045/202] Fix for utf8 encoding when catching lured pokemon (#2720) * Fixing lure pokestop encoding * fixing lure encoding --- pokemongo_bot/cell_workers/catch_lured_pokemon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemongo_bot/cell_workers/catch_lured_pokemon.py b/pokemongo_bot/cell_workers/catch_lured_pokemon.py index c0e9283b82..10a046dce9 100644 --- a/pokemongo_bot/cell_workers/catch_lured_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_lured_pokemon.py @@ -24,7 +24,7 @@ def get_lured_pokemon(self): details = fort_details(self.bot, fort_id=fort['id'], latitude=fort['latitude'], longitude=fort['longitude']) - fort_name = details.get('name', 'Unknown').encode('utf8', 'replace') + fort_name = details.get('name', 'Unknown') encounter_id = fort.get('lure_info', {}).get('encounter_id', None) @@ -32,7 +32,7 @@ def get_lured_pokemon(self): result = { 'encounter_id': encounter_id, 'fort_id': fort['id'], - 'fort_name': fort_name, + 'fort_name': u"{}".format(fort_name), 'latitude': fort['latitude'], 'longitude': fort['longitude'] } From 420c1be45dc39c42dcbea16a97975c81323bf9d9 Mon Sep 17 00:00:00 2001 From: AcorpBG Date: Sun, 7 Aug 2016 14:13:15 +0300 Subject: [PATCH 046/202] Fix For catchable not being displayed on the web (#2719) * Fix For catchable not being displayed on the web * Update catch_visible_pokemon.py --- pokemongo_bot/cell_workers/catch_visible_pokemon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py index 102d6c01c8..1bfed225df 100644 --- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -16,9 +16,9 @@ def work(self): key= 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: + with open(user_web_catchable, 'w') as outfile: json.dump(pokemon, outfile) self.emit_event( 'catchable_pokemon', From 77200afe72f57da59ed001d4d9eb6efe46ae172c Mon Sep 17 00:00:00 2001 From: Arthur Caranta Date: Sun, 7 Aug 2016 16:05:47 +0200 Subject: [PATCH 047/202] Added encrypt.so compilation process to Dockerfile (#2695) --- Dockerfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 58c45cd02f..e94035732a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,12 @@ RUN echo $timezone > /etc/timezone \ RUN apt-get update \ && apt-get install -y python-protobuf +RUN cd /tmp && wget "http://pgoapi.com/pgoencrypt.tar.gz" \ + && tar zxvf pgoencrypt.tar.gz \ + && cd pgoencrypt/src \ + && make \ + && cp libencrypt.so /usr/src/app/encrypt.so VOLUME ["/usr/src/app/web"] -ENTRYPOINT ["python", "pokecli.py"] \ No newline at end of file +ENTRYPOINT ["python", "pokecli.py"] From e927195fec5883c11a8e95dd6046856063c4931c Mon Sep 17 00:00:00 2001 From: Sander Date: Sun, 7 Aug 2016 16:29:11 +0200 Subject: [PATCH 048/202] OS Detection for encrypt lib (#2768) Fix 32bit check, darwin and linux use the same file Make it a function Check if file exists, if not show error Define file_name first Fix return Check if file exists, if not show error Print info about paths Fix for 32/64bit detection --- pokemongo_bot/__init__.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index c31c1085b3..f60594b8d8 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -28,8 +28,8 @@ from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl 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): @@ -591,6 +591,29 @@ def login(self): formatted="Login successful." ) + def get_encryption_lib(self): + file_name = '' + if _platform == "linux" or _platform == "linux2" or _platform == "darwin": + file_name = 'encrypt.so' + elif _platform == "Windows" or _platform == "win32": + # Check if we are on 32 or 64 bit + if sys.maxsize > 2**32: + file_name = 'encrypt_64.dll' + else: + file_name = 'encrypt.dll' + + path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) + full_path = path + '/'+ file_name + + if not os.path.isfile(full_path): + self.logger.error(file_name + ' is not found! Please place it in the bots root directory.') + self.logger.info('Platform: '+ _platform + ' Bot root directory: '+ path) + sys.exit(1) + else: + self.logger.info('Found '+ file_name +'! Platform: ' + _platform + ' Bot root directory: ' + path) + + return full_path + def _setup_api(self): # instantiate pgoapi self.api = ApiWrapper() @@ -602,8 +625,7 @@ def _setup_api(self): # chain subrequests (methods) into one RPC call self._print_character_info() - - self.api.activate_signature("encrypt.so") + self.api.activate_signature(self.get_encryption_lib()) self.logger.info('') self.update_inventory() # send empty map_cells and then our position From 4f7888bd5712c63c37f47d5c686d5a66a5c60b1e Mon Sep 17 00:00:00 2001 From: Peter Bonanni Date: Sun, 7 Aug 2016 09:32:30 -0500 Subject: [PATCH 049/202] Fix Typo in unexpected_response_retry (#2531) fixes #2525 #2523 From e8f804a5b853fb65c3789c9b7a364a5d8398f36f Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Sun, 7 Aug 2016 16:33:20 +0200 Subject: [PATCH 050/202] Revert "changing license from MIT to GPLv3" This reverts commit 69fb64f2bf7c12e28c2bb6d2b636c6af55822448. --- LICENSE | 676 +------------------------------------------------------- 1 file changed, 5 insertions(+), 671 deletions(-) diff --git a/LICENSE b/LICENSE index 9cecc1d466..d71a77b230 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,8 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 +The MIT License (MIT) +Copyright (c) 2016 PokemonGoF Team - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - Preamble +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {one line to give the program's name and a brief idea of what it does.} - Copyright (C) {year} {name of author} - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - {project} Copyright (C) {year} {fullname} - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 4eb7b388d950ff9d9517b5223967aa10adfbb10d Mon Sep 17 00:00:00 2001 From: Sander Date: Sun, 7 Aug 2016 16:44:14 +0200 Subject: [PATCH 051/202] When the google analytics domain is blocked the bot crashed. (#2764) With a simple try / except this can be solved. Fix dirty catch all --- pokemongo_bot/health_record/bot_event.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pokemongo_bot/health_record/bot_event.py b/pokemongo_bot/health_record/bot_event.py index 36c323f791..f357a4e8e7 100644 --- a/pokemongo_bot/health_record/bot_event.py +++ b/pokemongo_bot/health_record/bot_event.py @@ -73,8 +73,10 @@ def track_url(self, path): 't': 'pageview', 'dp': path } + try: + response = requests.post( + 'http://www.google-analytics.com/collect', data=data) - response = requests.post( - 'http://www.google-analytics.com/collect', data=data) - - response.raise_for_status() + response.raise_for_status() + except requests.exceptions.HTTPError: + pass From 6960f356a8d841cefbfd1018b23877b73a6a0d12 Mon Sep 17 00:00:00 2001 From: geek-man Date: Mon, 8 Aug 2016 01:14:55 +1000 Subject: [PATCH 052/202] Fixes #2698 - Prevents "Possibly searching too often" error after re-login. (#2771) * Fixes #2698 - Added api.activate_signature call to prevent issue after re-login. - Also replaced deprecated log call with event_manager emit to prevent exception being thrown. * Modified to use OS detected library path as per PR #2768 --- pokemongo_bot/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index f60594b8d8..24aaeb4b2d 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -547,11 +547,17 @@ def check_session(self, position): self.api._auth_provider._ticket_expire / 1000 - time.time() if remaining_time < 60: - self.logger.info("Session stale, re-logging in", 'yellow') + self.event_manager.emit( + 'api_error', + sender=self, + level='info', + formatted='Session stale, re-logging in.' + ) position = self.position self.api = ApiWrapper() self.position = position self.login() + self.api.activate_signature(self.get_encryption_lib()) @staticmethod def is_numeric(s): From eeecbc6c48d292fc916e4830fba1df290adb6ec4 Mon Sep 17 00:00:00 2001 From: Eli White Date: Sun, 7 Aug 2016 08:55:27 -0700 Subject: [PATCH 053/202] Support loading plugins from .zip files (#2766) --- pokemongo_bot/plugin_loader.py | 23 +++++++++++++++--- pokemongo_bot/test/plugin_loader_test.py | 8 ++++++ .../test/resources/plugin_fixture_test.zip | Bin 0 -> 3412 bytes 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 pokemongo_bot/test/resources/plugin_fixture_test.zip diff --git a/pokemongo_bot/plugin_loader.py b/pokemongo_bot/plugin_loader.py index f3c1fd9c2f..3bded030b3 100644 --- a/pokemongo_bot/plugin_loader.py +++ b/pokemongo_bot/plugin_loader.py @@ -5,11 +5,26 @@ class PluginLoader(object): folder_cache = [] + def _get_correct_path(self, path): + extension = os.path.splitext(path)[1] + + if extension == '.zip': + correct_path = path + else: + correct_path = os.path.dirname(path) + + return correct_path + def load_path(self, path): - parent_dir = os.path.dirname(path) - if parent_dir not in self.folder_cache: - self.folder_cache.append(parent_dir) - sys.path.append(parent_dir) + correct_path = self._get_correct_path(path) + + if correct_path not in self.folder_cache: + self.folder_cache.append(correct_path) + sys.path.append(correct_path) + + def remove_path(self, path): + correct_path = self._get_correct_path(path) + sys.path.remove(correct_path) def get_class(self, namespace_class): [namespace, class_name] = namespace_class.split('.') diff --git a/pokemongo_bot/test/plugin_loader_test.py b/pokemongo_bot/test/plugin_loader_test.py index 2960c5d36b..4d4d5ca952 100644 --- a/pokemongo_bot/test/plugin_loader_test.py +++ b/pokemongo_bot/test/plugin_loader_test.py @@ -18,3 +18,11 @@ def test_load_namespace_class(self): self.plugin_loader.load_path(package_path) loaded_class = self.plugin_loader.get_class('plugin_fixture.FakeTask') self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + self.plugin_loader.remove_path(package_path) + + def test_load_zip(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture_test.zip') + self.plugin_loader.load_path(package_path) + loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + self.plugin_loader.remove_path(package_path) diff --git a/pokemongo_bot/test/resources/plugin_fixture_test.zip b/pokemongo_bot/test/resources/plugin_fixture_test.zip new file mode 100644 index 0000000000000000000000000000000000000000..335d95e5226056902bb02114fa26a2b3dad61ca1 GIT binary patch literal 3412 zcmb7`dpuNmAIFdDjNGOgOqm!(61n7>TxMKy88Xx`+>91!# zS^64&kyMF7CH9A-CuBq$WQv$0!|~I2>_j&EdT`Jc&7K!`;P7HO7Xlsz4+J>05>PZz z6fh^oyqq5~{BlZwVvtDAIBOSouQ?5&#-VFL!{u*-G+ew*&^VT&mLJEq)lk|bfs>(- zoki#|to+?!_A|~-HQQQSNU7tZn3mANA z!?Sw^pf*Rr>#I@rq4>2?7GM+-DIhq2K_Y3>BOt3B2i0o{mN zd(<`VsMFc*N*7tf0%rCHV20Z7{n0L*dNk1Sgdc8eLOp?%Nx^OPlAS9`um8M=30ROj zUr|f_k6FDa%0g8O?*cIu?0w`%M>5QwIaJeKyE$S0P zGoyWhQ+*qg+b)c5E9@4^_EvgRf5lJl$PZ_@j>ZN{lvY(Kxf zYxMD#eN5_0N?F?k5if`&3lH!s@{g0rvJH1*Ag1&1368JUQjn9p_K+INV|i+RB#pX z4WlkIQNyI-HF3Hq)_b!@w646?vX|ta#56j{?S(4kr_Z6H9X3bKia(gV`)ND~lfurN zG)T!6cdW(P<#2zHA>Aj~SG>X=JUD7TggWwDaDS+d%pq}(`hbtl#e8mAMgEVsho4Mc z9aFkN_j~>Um8dM{xkvvwO+@tUQ{9|SOUZXIs5N~{q(daw8h1ros0gqHZ)^YXMWw5& zV+|IO-O9K)QT*fo& zy0aoe29iHTb{_Bia?Fujx&K9a#k58|(o$Rv`v~J#< zXn$Ac5$u4{Wcf!%zW;?XJ0yd3vgKBgLArnQ$ezl&hlj`WtvmuF@|)x9C+^rvs=eVP zb;KLHj9JYk)`M#wr|%L^5S`o9cGNNl!;L)P#YvhPtf6lS zKQV~w%y6jY6VdCO?4J3V!vB0!b$@+I4(8s)B$4mCI|{r0V{u0@`tHq`@XqL#nftrA))*>! z#(3z=|Bj%!Z4vhJiIPbD!q)_U{deA{&b5BJ#jeaaeac_wNjAD!Dzy?pyFPr;V}Gxq zLUPp8te`HKwn_2kt@{1eZ621gHfL`S;y(#PU23MFHbSOMS<#eQ|E|k_`16rl_o6>B z|Aj~45Tl=CI0f=+i=`d%JJZ!@=5Pw4q>V?nQjg8X+{jC#y$W%mkL;Cq zJ-zosbmHZRY%TQ@^-mi*-&zPk(^D&EMt%c&^xd3Cg@olq1Z6ELaK3W~b_@r7FTd5p z7n2tO57EAE^t>Tt6Y!Omd(uh(jc2ciFQ%}0?DA1Wf(JW`a{=303?@Xcc?1+hRzmPr zHj9hrMF?tmA=@l%m^WE$ Date: Sun, 7 Aug 2016 12:13:59 -0400 Subject: [PATCH 054/202] Update Readme (#2847) Fix typos --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ba131d5c39..f269d43e6d 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ The project is currently setup in two main branches. `dev` and `master`. ## Please submit PR to [Dev branch](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) -## Where to get the dll/so ? A help channel is comming. -You need grab them from internet. +## Where to get the DLL/SO? A help channel is coming. +You need to grab them from the Internet. We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) From 2750255a974ac1d2c83db26a47b19092d45293d4 Mon Sep 17 00:00:00 2001 From: Jeremy Bi Date: Mon, 8 Aug 2016 02:22:13 +0800 Subject: [PATCH 055/202] Keep track of how many pokemon released (#2884) --- pokemongo_bot/cell_workers/transfer_pokemon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index c48e17b20d..5c1d30fae7 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -187,6 +187,7 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): def release_pokemon(self, pokemon_name, cp, iv, pokemon_id): response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon_id) + self.bot.metrics.released_pokemon() self.emit_event( 'pokemon_release', formatted='Exchanged {pokemon} [CP {cp}] [IV {iv}] for candy.', From 823ba83d950ea8777aa774de87bb98543cc4055b Mon Sep 17 00:00:00 2001 From: Bernardo Vale Date: Sun, 7 Aug 2016 15:29:42 -0300 Subject: [PATCH 056/202] Setting Library path to work with encrypt.so (#2899) Setting LD_LIBRARY_PATH on Dockerfile --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index e94035732a..dce398c63e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,4 +15,6 @@ RUN cd /tmp && wget "http://pgoapi.com/pgoencrypt.tar.gz" \ VOLUME ["/usr/src/app/web"] +ENV LD_LIBRARY_PATH /usr/src/app + ENTRYPOINT ["python", "pokecli.py"] From fc4e802dd760508e45c9e2f79cdc9704a514b896 Mon Sep 17 00:00:00 2001 From: Genesis Date: Sun, 7 Aug 2016 20:42:35 +0200 Subject: [PATCH 057/202] :sparkles: Added login and username to available stats (#2494) Added a player_data property in PokemonGoBot to access player data from outside Added unit tests for login and username stats Added tests for call args when updating the window title Added a platform-specific test for window title updating on win32 platform --- CONTRIBUTORS.md | 1 + pokemongo_bot/__init__.py | 9 +++ .../cell_workers/update_title_stats.py | 12 +++- tests/update_title_stats_test.py | 57 ++++++++++++------- 4 files changed, 57 insertions(+), 22 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index dfd8a24af2..5a57ad6f8a 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -51,3 +51,4 @@ * matheussampaio * Abraxas000 * lucasfevi + * Moonlight-Angel diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 24aaeb4b2d..d0f034f110 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -39,6 +39,15 @@ def position(self): def position(self, position_tuple): self.api._position_lat, self.api._position_lng, self.api._position_alt = position_tuple + @property + def player_data(self): + """ + Returns the player data as received from the API. + :return: The player data. + :rtype: dict + """ + return self._player + def __init__(self, config): self.config = config self.fort_timeouts = dict() diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index 1f9b2a0e9b..43e55c260f 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -18,11 +18,13 @@ class UpdateTitleStats(BaseTask): "type": "UpdateTitleStats", "config": { "min_interval": 10, - "stats": ["uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"] + "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"] } } Available stats : + - login : The account login (from the credentials). + - username : The trainer name (asked at first in-game connection). - uptime : The bot uptime. - km_walked : The kilometers walked since the bot started. - level : The current character's level. @@ -107,8 +109,7 @@ def _update_title(self, title, platform): :rtype: None :raise: RuntimeError: When the given platform isn't supported. """ - if platform == "linux" or platform == "linux2"\ - or platform == "cygwin": + if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) elif platform == "darwin": stdout.write("\033]0;{}\007".format(title)) @@ -145,6 +146,9 @@ def _get_stats_title(self, player_stats): metrics = self.bot.metrics metrics.capture_stats() runtime = metrics.runtime() + login = self.bot.config.username + player_data = self.bot.player_data + username = player_data.get('username', '?') distance_travelled = metrics.distance_travelled() current_level = int(player_stats.get('level', 0)) prev_level_xp = int(player_stats.get('prev_level_xp', 0)) @@ -172,6 +176,8 @@ def _get_stats_title(self, player_stats): # Create stats strings. available_stats = { + 'login': login, + 'username': username, 'uptime': 'Uptime : {}'.format(runtime), 'km_walked': '{:,.2f}km walked'.format(distance_travelled), 'level': 'Level {}'.format(current_level), diff --git a/tests/update_title_stats_test.py b/tests/update_title_stats_test.py index 699d736b7a..ba480f0151 100644 --- a/tests/update_title_stats_test.py +++ b/tests/update_title_stats_test.py @@ -1,6 +1,7 @@ import unittest +from sys import platform as _platform from datetime import datetime, timedelta -from mock import patch, MagicMock +from mock import call, patch, MagicMock from pokemongo_bot.cell_workers.update_title_stats import UpdateTitleStats from tests import FakeBot @@ -8,11 +9,11 @@ class UpdateTitleStatsTestCase(unittest.TestCase): config = { 'min_interval': 20, - 'stats': ['pokemon_evolved', 'pokemon_encountered', 'uptime', 'pokemon_caught', - 'stops_visited', 'km_walked', 'level', 'stardust_earned', 'level_completion', - 'xp_per_hour', 'pokeballs_thrown', 'highest_cp_pokemon', 'level_stats', - 'xp_earned', 'pokemon_unseen', 'most_perfect_pokemon', 'pokemon_stats', - 'pokemon_released'] + 'stats': ['login', 'username', 'pokemon_evolved', 'pokemon_encountered', 'uptime', + 'pokemon_caught', 'stops_visited', 'km_walked', 'level', 'stardust_earned', + 'level_completion', 'xp_per_hour', 'pokeballs_thrown', 'highest_cp_pokemon', + 'level_stats', 'xp_earned', 'pokemon_unseen', 'most_perfect_pokemon', + 'pokemon_stats', 'pokemon_released'] } player_stats = { 'level': 25, @@ -23,6 +24,8 @@ class UpdateTitleStatsTestCase(unittest.TestCase): def setUp(self): self.bot = FakeBot() + self.bot._player = {'username': 'Username'} + self.bot.config.username = 'Login' self.worker = UpdateTitleStats(self.bot, self.config) def mock_metrics(self): @@ -87,22 +90,37 @@ def test_next_update_after_update_title(self, mock_datetime): now + timedelta(seconds=self.config['min_interval'])) @patch('pokemongo_bot.cell_workers.update_title_stats.stdout') - def test_update_title_linux_osx(self, mock_stdout): - self.worker._update_title('', 'linux') + def test_update_title_linux_cygwin(self, mock_stdout): + self.worker._update_title('new title linux', 'linux') self.assertEqual(mock_stdout.write.call_count, 1) + self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title linux\x07')) - self.worker._update_title('', 'linux2') + self.worker._update_title('new title linux2', 'linux2') self.assertEqual(mock_stdout.write.call_count, 2) + self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title linux2\x07')) - self.worker._update_title('', 'darwin') + self.worker._update_title('new title cygwin', 'cygwin') self.assertEqual(mock_stdout.write.call_count, 3) + self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title cygwin\x07')) + + @patch('pokemongo_bot.cell_workers.update_title_stats.stdout') + def test_update_title_darwin(self, mock_stdout): + self.worker._update_title('new title darwin', 'darwin') + + self.assertEqual(mock_stdout.write.call_count, 1) + self.assertEqual(mock_stdout.write.call_args, call('\033]0;new title darwin\007')) + + @unittest.skipUnless(_platform.startswith("win"), "requires Windows") + @patch('pokemongo_bot.cell_workers.update_title_stats.ctypes') + def test_update_title_win32(self, mock_ctypes): + self.worker._update_title('new title win32', 'win32') - @unittest.skip("Didn't find a way to mock ctypes.windll.kernel32.SetConsoleTitleA") - def test_update_title_win32(self): - self.worker._update_title('', 'win32') + self.assertEqual(mock_ctypes.windll.kernel32.SetConsoleTitleA.call_count, 1) + self.assertEqual(mock_ctypes.windll.kernel32.SetConsoleTitleA.call_args, + call('new title win32')) def test_get_stats_title_player_stats_none(self): title = self.worker._get_stats_title(None) @@ -119,12 +137,13 @@ def test_get_stats(self): self.mock_metrics() title = self.worker._get_stats_title(self.player_stats) - expected = 'Evolved 12 pokemon | Encountered 130 pokemon | Uptime : 15:42:13 | ' \ - 'Caught 120 pokemon | Visited 220 stops | 42.05km walked | Level 25 | ' \ - 'Earned 24,069 Stardust | 87,500 / 150,000 XP (58%) | 1,337 XP/h | ' \ - 'Threw 145 pokeballs | Highest CP pokemon : highest_cp | ' \ - 'Level 25 (87,500 / 150,000, 58%) | +424,242 XP | ' \ - 'Encountered 3 new pokemon | Most perfect pokemon : most_perfect | ' \ + expected = 'Login | Username | Evolved 12 pokemon | Encountered 130 pokemon | ' \ + 'Uptime : 15:42:13 | Caught 120 pokemon | Visited 220 stops | ' \ + '42.05km walked | Level 25 | Earned 24,069 Stardust | ' \ + '87,500 / 150,000 XP (58%) | 1,337 XP/h | Threw 145 pokeballs | ' \ + 'Highest CP pokemon : highest_cp | Level 25 (87,500 / 150,000, 58%) | ' \ + '+424,242 XP | Encountered 3 new pokemon | ' \ + 'Most perfect pokemon : most_perfect | ' \ 'Encountered 130 pokemon, 120 caught, 30 released, 12 evolved, ' \ '3 never seen before | Released 30 pokemon' From e5b7eda6cd7054baadbd6440bf9b94d3d5cd88e3 Mon Sep 17 00:00:00 2001 From: mjmadsen Date: Sun, 7 Aug 2016 16:55:59 -0500 Subject: [PATCH 058/202] [dev] small fixes (#2912) * Fixed emit_event typo * Update CONTRIBUTORS.md * Changed initialization location for "bot" We use bot in main exception on 128 * Update pokecli.py --- CONTRIBUTORS.md | 1 + pokecli.py | 3 ++- pokemongo_bot/cell_workers/evolve_pokemon.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5a57ad6f8a..ee5aa7063d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -52,3 +52,4 @@ * Abraxas000 * lucasfevi * Moonlight-Angel + * mjmadsen diff --git a/pokecli.py b/pokecli.py index 38f33a035c..24a0f38ee3 100644 --- a/pokecli.py +++ b/pokecli.py @@ -52,8 +52,9 @@ logger.setLevel(logging.INFO) def main(): + bot = False + try: - bot = False logger.info('PokemonGO Bot v1.0') sys.stdout = codecs.getwriter('utf8')(sys.stdout) sys.stderr = codecs.getwriter('utf8')(sys.stderr) diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 74eb0abf79..c3903a685d 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -59,7 +59,7 @@ def _should_run(self): if result is 1: # Request success self.emit_event( 'used_lucky_egg', - formmated='Used lucky egg ({amount_left} left).', + formatted='Used lucky egg ({amount_left} left).', data={ 'amount_left': lucky_egg_count - 1 } From 5b3fe3d187b6b5fdfdede00f064bf7bcbd5a484d Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sun, 7 Aug 2016 15:18:09 -0700 Subject: [PATCH 059/202] Dev merge to master (#2939) * Adding plugin support (#2679) * Adding plugin support * Adding an empty __init__.py * Moving the base task to the project root (#2702) * Moving the base task to the project root * Moving the base class more * Changing the import again * Adding a heartbeat to the analytics (#2709) * Adding a heartbeat to the analytics * Heartbeat every 30 seconds, not every 5 * Don't double track clients * Fix 'local variable 'bot' referenced before assignment' * Providing an error if tasks don't work for the given api (#2732) * Fix for utf8 encoding when catching lured pokemon (#2720) * Fixing lure pokestop encoding * fixing lure encoding * Fix For catchable not being displayed on the web (#2719) * Fix For catchable not being displayed on the web * Update catch_visible_pokemon.py * Added encrypt.so compilation process to Dockerfile (#2695) * OS Detection for encrypt lib (#2768) Fix 32bit check, darwin and linux use the same file Make it a function Check if file exists, if not show error Define file_name first Fix return Check if file exists, if not show error Print info about paths Fix for 32/64bit detection * Fix Typo in unexpected_response_retry (#2531) fixes #2525 #2523 * Revert "changing license from MIT to GPLv3" This reverts commit 69fb64f2bf7c12e28c2bb6d2b636c6af55822448. * When the google analytics domain is blocked the bot crashed. (#2764) With a simple try / except this can be solved. Fix dirty catch all * Fixes #2698 - Prevents "Possibly searching too often" error after re-login. (#2771) * Fixes #2698 - Added api.activate_signature call to prevent issue after re-login. - Also replaced deprecated log call with event_manager emit to prevent exception being thrown. * Modified to use OS detected library path as per PR #2768 * Support loading plugins from .zip files (#2766) * Keep track of how many pokemon released (#2884) * Setting Library path to work with encrypt.so (#2899) Setting LD_LIBRARY_PATH on Dockerfile * :sparkles: Added login and username to available stats (#2494) Added a player_data property in PokemonGoBot to access player data from outside Added unit tests for login and username stats Added tests for call args when updating the window title Added a platform-specific test for window title updating on win32 platform * [dev] small fixes (#2912) * Fixed emit_event typo * Update CONTRIBUTORS.md * Changed initialization location for "bot" We use bot in main exception on 128 * Update pokecli.py --- CONTRIBUTORS.md | 2 + Dockerfile | 9 +- LICENSE | 676 +----------------- pokecli.py | 9 + pokemongo_bot/__init__.py | 52 +- pokemongo_bot/{cell_workers => }/base_task.py | 1 + pokemongo_bot/cell_workers/__init__.py | 3 +- .../cell_workers/catch_lured_pokemon.py | 8 +- .../cell_workers/catch_visible_pokemon.py | 8 +- .../cell_workers/collect_level_up_reward.py | 4 +- pokemongo_bot/cell_workers/evolve_pokemon.py | 5 +- pokemongo_bot/cell_workers/follow_cluster.py | 3 +- pokemongo_bot/cell_workers/follow_path.py | 4 +- pokemongo_bot/cell_workers/follow_spiral.py | 4 +- pokemongo_bot/cell_workers/handle_soft_ban.py | 4 +- pokemongo_bot/cell_workers/incubate_eggs.py | 4 +- pokemongo_bot/cell_workers/move_to_fort.py | 3 +- .../cell_workers/move_to_map_pokemon.py | 4 +- .../cell_workers/nickname_pokemon.py | 4 +- .../cell_workers/pokemon_catch_worker.py | 2 +- pokemongo_bot/cell_workers/recycle_items.py | 4 +- pokemongo_bot/cell_workers/sleep_schedule.py | 3 +- pokemongo_bot/cell_workers/spin_fort.py | 4 +- .../cell_workers/transfer_pokemon.py | 5 +- .../cell_workers/update_title_stats.py | 15 +- pokemongo_bot/health_record/bot_event.py | 48 +- pokemongo_bot/plugin_loader.py | 33 + pokemongo_bot/test/plugin_loader_test.py | 28 + pokemongo_bot/test/resources/__init__.py | 0 .../test/resources/plugin_fixture/__init__.py | 2 + .../resources/plugin_fixture/fake_task.py | 7 + .../plugin_fixture/unsupported_api_task.py | 7 + .../test/resources/plugin_fixture_test.zip | Bin 0 -> 3412 bytes pokemongo_bot/tree_config_builder.py | 33 +- tests/base_task_test.py | 2 +- tests/tree_config_builder_test.py | 55 +- tests/update_title_stats_test.py | 57 +- 37 files changed, 367 insertions(+), 745 deletions(-) rename pokemongo_bot/{cell_workers => }/base_task.py (96%) create mode 100644 pokemongo_bot/plugin_loader.py create mode 100644 pokemongo_bot/test/plugin_loader_test.py create mode 100644 pokemongo_bot/test/resources/__init__.py create mode 100644 pokemongo_bot/test/resources/plugin_fixture/__init__.py create mode 100644 pokemongo_bot/test/resources/plugin_fixture/fake_task.py create mode 100644 pokemongo_bot/test/resources/plugin_fixture/unsupported_api_task.py create mode 100644 pokemongo_bot/test/resources/plugin_fixture_test.zip diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index dfd8a24af2..ee5aa7063d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -51,3 +51,5 @@ * matheussampaio * Abraxas000 * lucasfevi + * Moonlight-Angel + * mjmadsen diff --git a/Dockerfile b/Dockerfile index 58c45cd02f..dce398c63e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,14 @@ RUN echo $timezone > /etc/timezone \ RUN apt-get update \ && apt-get install -y python-protobuf +RUN cd /tmp && wget "http://pgoapi.com/pgoencrypt.tar.gz" \ + && tar zxvf pgoencrypt.tar.gz \ + && cd pgoencrypt/src \ + && make \ + && cp libencrypt.so /usr/src/app/encrypt.so VOLUME ["/usr/src/app/web"] -ENTRYPOINT ["python", "pokecli.py"] \ No newline at end of file +ENV LD_LIBRARY_PATH /usr/src/app + +ENTRYPOINT ["python", "pokecli.py"] diff --git a/LICENSE b/LICENSE index 9cecc1d466..d71a77b230 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,8 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 +The MIT License (MIT) +Copyright (c) 2016 PokemonGoF Team - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - Preamble +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {one line to give the program's name and a brief idea of what it does.} - Copyright (C) {year} {name of author} - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - {project} Copyright (C) {year} {fullname} - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pokecli.py b/pokecli.py index 3d1b3736e7..24a0f38ee3 100644 --- a/pokecli.py +++ b/pokecli.py @@ -40,6 +40,7 @@ from pokemongo_bot import PokemonGoBot, TreeConfigBuilder from pokemongo_bot.health_record import BotEvent +from pokemongo_bot.plugin_loader import PluginLoader if sys.version_info >= (2, 7, 9): ssl._create_default_https_context = ssl._create_unverified_context @@ -51,6 +52,8 @@ logger.setLevel(logging.INFO) def main(): + bot = False + try: logger.info('PokemonGO Bot v1.0') sys.stdout = codecs.getwriter('utf8')(sys.stdout) @@ -73,6 +76,7 @@ def main(): tree = TreeConfigBuilder(bot, config.raw_tasks).build() bot.workers = tree bot.metrics.capture_stats() + bot.health_record = health_record bot.event_manager.emit( 'bot_start', @@ -384,6 +388,7 @@ def init_config(): config.release = load.get('release', {}) config.action_wait_max = load.get('action_wait_max', 4) config.action_wait_min = load.get('action_wait_min', 1) + config.plugins = load.get('plugins', []) config.raw_tasks = load.get('tasks', []) config.vips = load.get('vips', {}) @@ -439,6 +444,10 @@ def task_configuration_error(flag_name): parser.error("--catch_randomize_spin_factor is out of range! (should be 0 <= catch_randomize_spin_factor <= 1)") return None + plugin_loader = PluginLoader() + for plugin in config.plugins: + plugin_loader.load_path(plugin) + # create web dir if not exists try: os.makedirs(web_dir) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 1d318f473b..d0f034f110 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -15,6 +15,8 @@ from pgoapi.utilities import f2i, get_cell_ids import cell_workers +from base_task import BaseTask +from plugin_loader import PluginLoader from api_wrapper import ApiWrapper from cell_workers.utils import distance from event_manager import EventManager @@ -25,9 +27,9 @@ from pokemongo_bot.socketio_server.runner import SocketIoRunner from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl from worker_result import WorkerResult -from tree_config_builder import ConfigException, TreeConfigBuilder - - +from tree_config_builder import ConfigException, MismatchTaskApiVersion, TreeConfigBuilder +from sys import platform as _platform +import struct class PokemonGoBot(object): @property def position(self): @@ -37,6 +39,15 @@ def position(self): def position(self, position_tuple): self.api._position_lat, self.api._position_lng, self.api._position_alt = position_tuple + @property + def player_data(self): + """ + Returns the player data as received from the API. + :return: The player data. + :rtype: dict + """ + return self._player + def __init__(self, config): self.config = config self.fort_timeouts = dict() @@ -383,6 +394,7 @@ def _register_events(self): self.event_manager.register_event('unset_pokemon_nickname') def tick(self): + self.health_record.heartbeat() self.cell = self.get_meta_cell() self.tick_count += 1 @@ -544,11 +556,17 @@ def check_session(self, position): self.api._auth_provider._ticket_expire / 1000 - time.time() if remaining_time < 60: - self.logger.info("Session stale, re-logging in", 'yellow') + self.event_manager.emit( + 'api_error', + sender=self, + level='info', + formatted='Session stale, re-logging in.' + ) position = self.position self.api = ApiWrapper() self.position = position self.login() + self.api.activate_signature(self.get_encryption_lib()) @staticmethod def is_numeric(s): @@ -588,6 +606,29 @@ def login(self): formatted="Login successful." ) + def get_encryption_lib(self): + file_name = '' + if _platform == "linux" or _platform == "linux2" or _platform == "darwin": + file_name = 'encrypt.so' + elif _platform == "Windows" or _platform == "win32": + # Check if we are on 32 or 64 bit + if sys.maxsize > 2**32: + file_name = 'encrypt_64.dll' + else: + file_name = 'encrypt.dll' + + path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) + full_path = path + '/'+ file_name + + if not os.path.isfile(full_path): + self.logger.error(file_name + ' is not found! Please place it in the bots root directory.') + self.logger.info('Platform: '+ _platform + ' Bot root directory: '+ path) + sys.exit(1) + else: + self.logger.info('Found '+ file_name +'! Platform: ' + _platform + ' Bot root directory: ' + path) + + return full_path + def _setup_api(self): # instantiate pgoapi self.api = ApiWrapper() @@ -599,8 +640,7 @@ def _setup_api(self): # chain subrequests (methods) into one RPC call self._print_character_info() - - self.api.activate_signature("encrypt.so") + self.api.activate_signature(self.get_encryption_lib()) self.logger.info('') self.update_inventory() # send empty map_cells and then our position diff --git a/pokemongo_bot/cell_workers/base_task.py b/pokemongo_bot/base_task.py similarity index 96% rename from pokemongo_bot/cell_workers/base_task.py rename to pokemongo_bot/base_task.py index ac48b9a676..22bbedf4e8 100644 --- a/pokemongo_bot/cell_workers/base_task.py +++ b/pokemongo_bot/base_task.py @@ -2,6 +2,7 @@ class BaseTask(object): + TASK_API_VERSION = 1 def __init__(self, bot, config): self.bot = bot diff --git a/pokemongo_bot/cell_workers/__init__.py b/pokemongo_bot/cell_workers/__init__.py index bc6638d1fd..68d181947a 100644 --- a/pokemongo_bot/cell_workers/__init__.py +++ b/pokemongo_bot/cell_workers/__init__.py @@ -15,7 +15,6 @@ from follow_path import FollowPath from follow_spiral import FollowSpiral from collect_level_up_reward import CollectLevelUpReward -from base_task import BaseTask from follow_cluster import FollowCluster from sleep_schedule import SleepSchedule -from update_title_stats import UpdateTitleStats \ No newline at end of file +from update_title_stats import UpdateTitleStats diff --git a/pokemongo_bot/cell_workers/catch_lured_pokemon.py b/pokemongo_bot/cell_workers/catch_lured_pokemon.py index bf2d45bb4b..10a046dce9 100644 --- a/pokemongo_bot/cell_workers/catch_lured_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_lured_pokemon.py @@ -3,10 +3,12 @@ from pokemongo_bot.cell_workers.utils import fort_details from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class CatchLuredPokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def work(self): lured_pokemon = self.get_lured_pokemon() if lured_pokemon: @@ -22,7 +24,7 @@ def get_lured_pokemon(self): details = fort_details(self.bot, fort_id=fort['id'], latitude=fort['latitude'], longitude=fort['longitude']) - fort_name = details.get('name', 'Unknown').encode('utf8', 'replace') + fort_name = details.get('name', 'Unknown') encounter_id = fort.get('lure_info', {}).get('encounter_id', None) @@ -30,7 +32,7 @@ def get_lured_pokemon(self): result = { 'encounter_id': encounter_id, 'fort_id': fort['id'], - 'fort_name': fort_name, + 'fort_name': u"{}".format(fort_name), 'latitude': fort['latitude'], 'longitude': fort['longitude'] } diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py index 1103815a4c..1bfed225df 100644 --- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -1,11 +1,13 @@ import json -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker from utils import distance class CatchVisiblePokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def work(self): if 'catchable_pokemons' in self.bot.cell and len(self.bot.cell['catchable_pokemons']) > 0: # Sort all by distance from current pos- eventually this should @@ -14,9 +16,9 @@ def work(self): key= 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: + with open(user_web_catchable, 'w') as outfile: json.dump(pokemon, outfile) self.emit_event( 'catchable_pokemon', diff --git a/pokemongo_bot/cell_workers/collect_level_up_reward.py b/pokemongo_bot/cell_workers/collect_level_up_reward.py index 304818fe2b..950f450660 100644 --- a/pokemongo_bot/cell_workers/collect_level_up_reward.py +++ b/pokemongo_bot/cell_workers/collect_level_up_reward.py @@ -1,7 +1,9 @@ -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class CollectLevelUpReward(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + current_level = 0 previous_level = 0 diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 911d6a1f67..c3903a685d 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -1,9 +1,10 @@ from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.item_list import Item -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class EvolvePokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 def initialize(self): self.api = self.bot.api @@ -58,7 +59,7 @@ def _should_run(self): if result is 1: # Request success self.emit_event( 'used_lucky_egg', - formmated='Used lucky egg ({amount_left} left).', + formatted='Used lucky egg ({amount_left} left).', data={ 'amount_left': lucky_egg_count - 1 } diff --git a/pokemongo_bot/cell_workers/follow_cluster.py b/pokemongo_bot/cell_workers/follow_cluster.py index 02d3880a7e..8448fcf742 100644 --- a/pokemongo_bot/cell_workers/follow_cluster.py +++ b/pokemongo_bot/cell_workers/follow_cluster.py @@ -1,9 +1,10 @@ from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.cell_workers.utils import distance from pokemongo_bot.cell_workers.utils import find_biggest_cluster -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class FollowCluster(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 def initialize(self): self.is_at_destination = False diff --git a/pokemongo_bot/cell_workers/follow_path.py b/pokemongo_bot/cell_workers/follow_path.py index 04eb817593..6e183ed1d7 100644 --- a/pokemongo_bot/cell_workers/follow_path.py +++ b/pokemongo_bot/cell_workers/follow_path.py @@ -3,7 +3,7 @@ import gpxpy import gpxpy.gpx import json -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.utils import distance, i2f, format_dist from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.step_walker import StepWalker @@ -11,6 +11,8 @@ class FollowPath(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): self.ptr = 0 self._process_config() diff --git a/pokemongo_bot/cell_workers/follow_spiral.py b/pokemongo_bot/cell_workers/follow_spiral.py index 28b548d1ca..f175369e45 100644 --- a/pokemongo_bot/cell_workers/follow_spiral.py +++ b/pokemongo_bot/cell_workers/follow_spiral.py @@ -5,9 +5,11 @@ 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 +from pokemongo_bot.base_task import BaseTask class FollowSpiral(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): self.steplimit = self.config.get("diameter", 4) self.step_size = self.config.get("step_size", 70) diff --git a/pokemongo_bot/cell_workers/handle_soft_ban.py b/pokemongo_bot/cell_workers/handle_soft_ban.py index 4f9d416e83..8018b7c33e 100644 --- a/pokemongo_bot/cell_workers/handle_soft_ban.py +++ b/pokemongo_bot/cell_workers/handle_soft_ban.py @@ -3,13 +3,15 @@ from pgoapi.utilities import f2i from pokemongo_bot.constants import Constants -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers import MoveToFort from pokemongo_bot.cell_workers.utils import distance from pokemongo_bot.worker_result import WorkerResult class HandleSoftBan(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def work(self): if not self.should_run(): return diff --git a/pokemongo_bot/cell_workers/incubate_eggs.py b/pokemongo_bot/cell_workers/incubate_eggs.py index 9e21b0d280..5761090ea5 100644 --- a/pokemongo_bot/cell_workers/incubate_eggs.py +++ b/pokemongo_bot/cell_workers/incubate_eggs.py @@ -1,8 +1,10 @@ from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class IncubateEggs(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + last_km_walked = 0 def initialize(self): diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index f43d1641e6..e4b4187d20 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -4,11 +4,12 @@ from pokemongo_bot.constants import Constants from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from utils import distance, format_dist, fort_details class MoveToFort(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 def initialize(self): self.lure_distance = 0 diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index bce39e0143..08ff35f281 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -9,11 +9,13 @@ from pokemongo_bot.cell_workers.utils import distance, format_dist, format_time from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker class MoveToMapPokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): self.last_map_update = 0 self.pokemon_data = self.bot.pokemon_list diff --git a/pokemongo_bot/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py index 29df15ae4a..cda206ad20 100644 --- a/pokemongo_bot/cell_workers/nickname_pokemon.py +++ b/pokemongo_bot/cell_workers/nickname_pokemon.py @@ -1,7 +1,9 @@ from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class NicknamePokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): self.template = self.config.get('nickname_template','').lower().strip() if self.template == "{name}": diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index afa5578267..d676e9f0e2 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -3,7 +3,7 @@ import time from pokemongo_bot.human_behaviour import (normalized_reticle_size, sleep, spin_modifier) -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class PokemonCatchWorker(BaseTask): BAG_FULL = 'bag_full' diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index c28b2749b1..2c969913b0 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -1,9 +1,11 @@ import json import os -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.tree_config_builder import ConfigException class RecycleItems(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): self.item_filter = self.config.get('item_filter', {}) self._validate_item_filter() diff --git a/pokemongo_bot/cell_workers/sleep_schedule.py b/pokemongo_bot/cell_workers/sleep_schedule.py index daaf0b8f1e..5a7d617e23 100644 --- a/pokemongo_bot/cell_workers/sleep_schedule.py +++ b/pokemongo_bot/cell_workers/sleep_schedule.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta from time import sleep from random import uniform -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class SleepSchedule(BaseTask): @@ -27,6 +27,7 @@ class SleepSchedule(BaseTask): duration_random_offset: (HH:MM) random offset of duration of sleep for this example the possible duration is 5:00-6:00 """ + SUPPORTED_TASK_API_VERSION = 1 LOG_INTERVAL_SECONDS = 600 SCHEDULING_MARGIN = timedelta(minutes=10) # Skip if next sleep is RESCHEDULING_MARGIN from now diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 9572008241..e04a86dbc6 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -8,11 +8,13 @@ from pokemongo_bot.constants import Constants from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.worker_result import WorkerResult -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from utils import distance, format_time, fort_details class SpinFort(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def should_run(self): if not self.bot.has_space_for_loot(): self.emit_event( diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 70c5939c58..5c1d30fae7 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -1,10 +1,12 @@ import json from pokemongo_bot.human_behaviour import action_delay -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class TransferPokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def work(self): pokemon_groups = self._release_pokemon_get_groups() for pokemon_id in pokemon_groups: @@ -185,6 +187,7 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): def release_pokemon(self, pokemon_name, cp, iv, pokemon_id): response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon_id) + self.bot.metrics.released_pokemon() self.emit_event( 'pokemon_release', formatted='Exchanged {pokemon} [CP {cp}] [IV {iv}] for candy.', diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index 911d2efd4d..43e55c260f 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -2,7 +2,7 @@ from sys import stdout, platform as _platform from datetime import datetime, timedelta -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.worker_result import WorkerResult from pokemongo_bot.tree_config_builder import ConfigException @@ -18,11 +18,13 @@ class UpdateTitleStats(BaseTask): "type": "UpdateTitleStats", "config": { "min_interval": 10, - "stats": ["uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"] + "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"] } } Available stats : + - login : The account login (from the credentials). + - username : The trainer name (asked at first in-game connection). - uptime : The bot uptime. - km_walked : The kilometers walked since the bot started. - level : The current character's level. @@ -49,6 +51,7 @@ class UpdateTitleStats(BaseTask): stats : An array of stats to display and their display order (implicitly), see available stats above. """ + SUPPORTED_TASK_API_VERSION = 1 DEFAULT_MIN_INTERVAL = 10 DEFAULT_DISPLAYED_STATS = [] @@ -106,8 +109,7 @@ def _update_title(self, title, platform): :rtype: None :raise: RuntimeError: When the given platform isn't supported. """ - if platform == "linux" or platform == "linux2"\ - or platform == "cygwin": + if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) elif platform == "darwin": stdout.write("\033]0;{}\007".format(title)) @@ -144,6 +146,9 @@ def _get_stats_title(self, player_stats): metrics = self.bot.metrics metrics.capture_stats() runtime = metrics.runtime() + login = self.bot.config.username + player_data = self.bot.player_data + username = player_data.get('username', '?') distance_travelled = metrics.distance_travelled() current_level = int(player_stats.get('level', 0)) prev_level_xp = int(player_stats.get('prev_level_xp', 0)) @@ -171,6 +176,8 @@ def _get_stats_title(self, player_stats): # Create stats strings. available_stats = { + 'login': login, + 'username': username, 'uptime': 'Uptime : {}'.format(runtime), 'km_walked': '{:,.2f}km walked'.format(distance_travelled), 'level': 'Level {}'.format(current_level), diff --git a/pokemongo_bot/health_record/bot_event.py b/pokemongo_bot/health_record/bot_event.py index 986b5f3c70..f357a4e8e7 100644 --- a/pokemongo_bot/health_record/bot_event.py +++ b/pokemongo_bot/health_record/bot_event.py @@ -7,6 +7,7 @@ import os import uuid import requests +import time class BotEvent(object): def __init__(self, config): @@ -31,38 +32,51 @@ def __init__(self, config): context = {} ) + self.client_id = uuid.uuid4() + self.heartbeat_wait = 30 # seconds + self.last_heartbeat = time.time() + def capture_error(self): if self.config.health_record: self.client.captureException() def login_success(self): if self.config.health_record: - track_url('/loggedin') + self.last_heartbeat = time.time() + self.track_url('/loggedin') def login_failed(self): if self.config.health_record: - track_url('/login') + self.track_url('/login') def login_retry(self): if self.config.health_record: - track_url('/relogin') + self.track_url('/relogin') def logout(self): if self.config.health_record: - track_url('/logout') - + self.track_url('/logout') -def track_url(path): - data = { - 'v': '1', - 'tid': 'UA-81469507-1', - 'aip': '1', # Anonymize IPs - 'cid': uuid.uuid4(), - 't': 'pageview', - 'dp': path - } + def heartbeat(self): + if self.config.health_record: + current_time = time.time() + if current_time - self.heartbeat_wait > self.last_heartbeat: + self.last_heartbeat = current_time + self.track_url('/heartbeat') - response = requests.post( - 'http://www.google-analytics.com/collect', data=data) + def track_url(self, path): + data = { + 'v': '1', + 'tid': 'UA-81469507-1', + 'aip': '1', # Anonymize IPs + 'cid': self.client_id, + 't': 'pageview', + 'dp': path + } + try: + response = requests.post( + 'http://www.google-analytics.com/collect', data=data) - response.raise_for_status() + response.raise_for_status() + except requests.exceptions.HTTPError: + pass diff --git a/pokemongo_bot/plugin_loader.py b/pokemongo_bot/plugin_loader.py new file mode 100644 index 0000000000..3bded030b3 --- /dev/null +++ b/pokemongo_bot/plugin_loader.py @@ -0,0 +1,33 @@ +import os +import sys +import importlib + +class PluginLoader(object): + folder_cache = [] + + def _get_correct_path(self, path): + extension = os.path.splitext(path)[1] + + if extension == '.zip': + correct_path = path + else: + correct_path = os.path.dirname(path) + + return correct_path + + def load_path(self, path): + correct_path = self._get_correct_path(path) + + if correct_path not in self.folder_cache: + self.folder_cache.append(correct_path) + sys.path.append(correct_path) + + def remove_path(self, path): + correct_path = self._get_correct_path(path) + sys.path.remove(correct_path) + + def get_class(self, namespace_class): + [namespace, class_name] = namespace_class.split('.') + my_module = importlib.import_module(namespace) + return getattr(my_module, class_name) + diff --git a/pokemongo_bot/test/plugin_loader_test.py b/pokemongo_bot/test/plugin_loader_test.py new file mode 100644 index 0000000000..4d4d5ca952 --- /dev/null +++ b/pokemongo_bot/test/plugin_loader_test.py @@ -0,0 +1,28 @@ +import imp +import sys +import pkgutil +import importlib +import unittest +import os +from datetime import timedelta, datetime +from mock import patch, MagicMock +from pokemongo_bot.plugin_loader import PluginLoader +from pokemongo_bot.test.resources.plugin_fixture import FakeTask + +class PluginLoaderTest(unittest.TestCase): + def setUp(self): + self.plugin_loader = PluginLoader() + + def test_load_namespace_class(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') + self.plugin_loader.load_path(package_path) + loaded_class = self.plugin_loader.get_class('plugin_fixture.FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + self.plugin_loader.remove_path(package_path) + + def test_load_zip(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture_test.zip') + self.plugin_loader.load_path(package_path) + loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + self.plugin_loader.remove_path(package_path) diff --git a/pokemongo_bot/test/resources/__init__.py b/pokemongo_bot/test/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pokemongo_bot/test/resources/plugin_fixture/__init__.py b/pokemongo_bot/test/resources/plugin_fixture/__init__.py new file mode 100644 index 0000000000..57caf83dce --- /dev/null +++ b/pokemongo_bot/test/resources/plugin_fixture/__init__.py @@ -0,0 +1,2 @@ +from fake_task import FakeTask +from unsupported_api_task import UnsupportedApiTask diff --git a/pokemongo_bot/test/resources/plugin_fixture/fake_task.py b/pokemongo_bot/test/resources/plugin_fixture/fake_task.py new file mode 100644 index 0000000000..ff729adee4 --- /dev/null +++ b/pokemongo_bot/test/resources/plugin_fixture/fake_task.py @@ -0,0 +1,7 @@ +from pokemongo_bot.base_task import BaseTask + +class FakeTask(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + + def work(self): + return 'FakeTask' diff --git a/pokemongo_bot/test/resources/plugin_fixture/unsupported_api_task.py b/pokemongo_bot/test/resources/plugin_fixture/unsupported_api_task.py new file mode 100644 index 0000000000..871e38f82d --- /dev/null +++ b/pokemongo_bot/test/resources/plugin_fixture/unsupported_api_task.py @@ -0,0 +1,7 @@ +from pokemongo_bot.base_task import BaseTask + +class UnsupportedApiTask(BaseTask): + SUPPORTED_TASK_API_VERSION = 2 + + def work(): + return 2 diff --git a/pokemongo_bot/test/resources/plugin_fixture_test.zip b/pokemongo_bot/test/resources/plugin_fixture_test.zip new file mode 100644 index 0000000000000000000000000000000000000000..335d95e5226056902bb02114fa26a2b3dad61ca1 GIT binary patch literal 3412 zcmb7`dpuNmAIFdDjNGOgOqm!(61n7>TxMKy88Xx`+>91!# zS^64&kyMF7CH9A-CuBq$WQv$0!|~I2>_j&EdT`Jc&7K!`;P7HO7Xlsz4+J>05>PZz z6fh^oyqq5~{BlZwVvtDAIBOSouQ?5&#-VFL!{u*-G+ew*&^VT&mLJEq)lk|bfs>(- zoki#|to+?!_A|~-HQQQSNU7tZn3mANA z!?Sw^pf*Rr>#I@rq4>2?7GM+-DIhq2K_Y3>BOt3B2i0o{mN zd(<`VsMFc*N*7tf0%rCHV20Z7{n0L*dNk1Sgdc8eLOp?%Nx^OPlAS9`um8M=30ROj zUr|f_k6FDa%0g8O?*cIu?0w`%M>5QwIaJeKyE$S0P zGoyWhQ+*qg+b)c5E9@4^_EvgRf5lJl$PZ_@j>ZN{lvY(Kxf zYxMD#eN5_0N?F?k5if`&3lH!s@{g0rvJH1*Ag1&1368JUQjn9p_K+INV|i+RB#pX z4WlkIQNyI-HF3Hq)_b!@w646?vX|ta#56j{?S(4kr_Z6H9X3bKia(gV`)ND~lfurN zG)T!6cdW(P<#2zHA>Aj~SG>X=JUD7TggWwDaDS+d%pq}(`hbtl#e8mAMgEVsho4Mc z9aFkN_j~>Um8dM{xkvvwO+@tUQ{9|SOUZXIs5N~{q(daw8h1ros0gqHZ)^YXMWw5& zV+|IO-O9K)QT*fo& zy0aoe29iHTb{_Bia?Fujx&K9a#k58|(o$Rv`v~J#< zXn$Ac5$u4{Wcf!%zW;?XJ0yd3vgKBgLArnQ$ezl&hlj`WtvmuF@|)x9C+^rvs=eVP zb;KLHj9JYk)`M#wr|%L^5S`o9cGNNl!;L)P#YvhPtf6lS zKQV~w%y6jY6VdCO?4J3V!vB0!b$@+I4(8s)B$4mCI|{r0V{u0@`tHq`@XqL#nftrA))*>! z#(3z=|Bj%!Z4vhJiIPbD!q)_U{deA{&b5BJ#jeaaeac_wNjAD!Dzy?pyFPr;V}Gxq zLUPp8te`HKwn_2kt@{1eZ621gHfL`S;y(#PU23MFHbSOMS<#eQ|E|k_`16rl_o6>B z|Aj~45Tl=CI0f=+i=`d%JJZ!@=5Pw4q>V?nQjg8X+{jC#y$W%mkL;Cq zJ-zosbmHZRY%TQ@^-mi*-&zPk(^D&EMt%c&^xd3Cg@olq1Z6ELaK3W~b_@r7FTd5p z7n2tO57EAE^t>Tt6Y!Omd(uh(jc2ciFQ%}0?DA1Wf(JW`a{=303?@Xcc?1+hRzmPr zHj9hrMF?tmA=@l%m^WE$ worker.SUPPORTED_TASK_API_VERSION: + error_string = 'Is there a new version of this task?' + + if error_string != '': + raise MismatchTaskApiVersion( + 'Task {} only works with task api version {}, you are currently running version {}. {}' + .format( + task_type, + worker.SUPPORTED_TASK_API_VERSION, + BaseTask.TASK_API_VERSION, + error_string + ) + ) + instance = worker(self.bot, task_config) workers.append(instance) diff --git a/tests/base_task_test.py b/tests/base_task_test.py index 16684d900c..ee259f80dc 100644 --- a/tests/base_task_test.py +++ b/tests/base_task_test.py @@ -1,6 +1,6 @@ import unittest import json -from pokemongo_bot.cell_workers import BaseTask +from pokemongo_bot.base_task import BaseTask class FakeTask(BaseTask): def initialize(self): diff --git a/tests/tree_config_builder_test.py b/tests/tree_config_builder_test.py index cee1080280..cd8bed22e0 100644 --- a/tests/tree_config_builder_test.py +++ b/tests/tree_config_builder_test.py @@ -1,7 +1,9 @@ import unittest import json -from pokemongo_bot import PokemonGoBot, ConfigException, TreeConfigBuilder +import os +from pokemongo_bot import PokemonGoBot, ConfigException, MismatchTaskApiVersion, TreeConfigBuilder, PluginLoader, BaseTask from pokemongo_bot.cell_workers import HandleSoftBan, CatchLuredPokemon +from pokemongo_bot.test.resources.plugin_fixture import FakeTask, UnsupportedApiTask def convert_from_json(str): return json.loads(str) @@ -83,3 +85,54 @@ def test_task_with_config(self): builder = TreeConfigBuilder(self.bot, obj) tree = builder.build() self.assertTrue(tree[0].config.get('longer_eggs_first', False)) + + def test_load_plugin_task(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') + plugin_loader = PluginLoader() + plugin_loader.load_path(package_path) + + obj = convert_from_json("""[{ + "type": "plugin_fixture.FakeTask" + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + tree = builder.build() + result = tree[0].work() + self.assertEqual(result, 'FakeTask') + + def setupUnsupportedBuilder(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'pokemongo_bot', 'test', 'resources', 'plugin_fixture') + plugin_loader = PluginLoader() + plugin_loader.load_path(package_path) + + obj = convert_from_json("""[{ + "type": "plugin_fixture.UnsupportedApiTask" + }]""") + + return TreeConfigBuilder(self.bot, obj) + + def test_task_version_too_high(self): + builder = self.setupUnsupportedBuilder() + + previous_version = BaseTask.TASK_API_VERSION + BaseTask.TASK_API_VERSION = 1 + + self.assertRaisesRegexp( + MismatchTaskApiVersion, + "Task plugin_fixture.UnsupportedApiTask only works with task api version 2, you are currently running version 1. Do you need to update the bot?", + builder.build) + + BaseTask.TASK_API_VERSION = previous_version + + def test_task_version_too_low(self): + builder = self.setupUnsupportedBuilder() + + previous_version = BaseTask.TASK_API_VERSION + BaseTask.TASK_API_VERSION = 3 + + self.assertRaisesRegexp( + MismatchTaskApiVersion, + "Task plugin_fixture.UnsupportedApiTask only works with task api version 2, you are currently running version 3. Is there a new version of this task?", + builder.build) + + BaseTask.TASK_API_VERSION = previous_version diff --git a/tests/update_title_stats_test.py b/tests/update_title_stats_test.py index 699d736b7a..ba480f0151 100644 --- a/tests/update_title_stats_test.py +++ b/tests/update_title_stats_test.py @@ -1,6 +1,7 @@ import unittest +from sys import platform as _platform from datetime import datetime, timedelta -from mock import patch, MagicMock +from mock import call, patch, MagicMock from pokemongo_bot.cell_workers.update_title_stats import UpdateTitleStats from tests import FakeBot @@ -8,11 +9,11 @@ class UpdateTitleStatsTestCase(unittest.TestCase): config = { 'min_interval': 20, - 'stats': ['pokemon_evolved', 'pokemon_encountered', 'uptime', 'pokemon_caught', - 'stops_visited', 'km_walked', 'level', 'stardust_earned', 'level_completion', - 'xp_per_hour', 'pokeballs_thrown', 'highest_cp_pokemon', 'level_stats', - 'xp_earned', 'pokemon_unseen', 'most_perfect_pokemon', 'pokemon_stats', - 'pokemon_released'] + 'stats': ['login', 'username', 'pokemon_evolved', 'pokemon_encountered', 'uptime', + 'pokemon_caught', 'stops_visited', 'km_walked', 'level', 'stardust_earned', + 'level_completion', 'xp_per_hour', 'pokeballs_thrown', 'highest_cp_pokemon', + 'level_stats', 'xp_earned', 'pokemon_unseen', 'most_perfect_pokemon', + 'pokemon_stats', 'pokemon_released'] } player_stats = { 'level': 25, @@ -23,6 +24,8 @@ class UpdateTitleStatsTestCase(unittest.TestCase): def setUp(self): self.bot = FakeBot() + self.bot._player = {'username': 'Username'} + self.bot.config.username = 'Login' self.worker = UpdateTitleStats(self.bot, self.config) def mock_metrics(self): @@ -87,22 +90,37 @@ def test_next_update_after_update_title(self, mock_datetime): now + timedelta(seconds=self.config['min_interval'])) @patch('pokemongo_bot.cell_workers.update_title_stats.stdout') - def test_update_title_linux_osx(self, mock_stdout): - self.worker._update_title('', 'linux') + def test_update_title_linux_cygwin(self, mock_stdout): + self.worker._update_title('new title linux', 'linux') self.assertEqual(mock_stdout.write.call_count, 1) + self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title linux\x07')) - self.worker._update_title('', 'linux2') + self.worker._update_title('new title linux2', 'linux2') self.assertEqual(mock_stdout.write.call_count, 2) + self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title linux2\x07')) - self.worker._update_title('', 'darwin') + self.worker._update_title('new title cygwin', 'cygwin') self.assertEqual(mock_stdout.write.call_count, 3) + self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title cygwin\x07')) + + @patch('pokemongo_bot.cell_workers.update_title_stats.stdout') + def test_update_title_darwin(self, mock_stdout): + self.worker._update_title('new title darwin', 'darwin') + + self.assertEqual(mock_stdout.write.call_count, 1) + self.assertEqual(mock_stdout.write.call_args, call('\033]0;new title darwin\007')) + + @unittest.skipUnless(_platform.startswith("win"), "requires Windows") + @patch('pokemongo_bot.cell_workers.update_title_stats.ctypes') + def test_update_title_win32(self, mock_ctypes): + self.worker._update_title('new title win32', 'win32') - @unittest.skip("Didn't find a way to mock ctypes.windll.kernel32.SetConsoleTitleA") - def test_update_title_win32(self): - self.worker._update_title('', 'win32') + self.assertEqual(mock_ctypes.windll.kernel32.SetConsoleTitleA.call_count, 1) + self.assertEqual(mock_ctypes.windll.kernel32.SetConsoleTitleA.call_args, + call('new title win32')) def test_get_stats_title_player_stats_none(self): title = self.worker._get_stats_title(None) @@ -119,12 +137,13 @@ def test_get_stats(self): self.mock_metrics() title = self.worker._get_stats_title(self.player_stats) - expected = 'Evolved 12 pokemon | Encountered 130 pokemon | Uptime : 15:42:13 | ' \ - 'Caught 120 pokemon | Visited 220 stops | 42.05km walked | Level 25 | ' \ - 'Earned 24,069 Stardust | 87,500 / 150,000 XP (58%) | 1,337 XP/h | ' \ - 'Threw 145 pokeballs | Highest CP pokemon : highest_cp | ' \ - 'Level 25 (87,500 / 150,000, 58%) | +424,242 XP | ' \ - 'Encountered 3 new pokemon | Most perfect pokemon : most_perfect | ' \ + expected = 'Login | Username | Evolved 12 pokemon | Encountered 130 pokemon | ' \ + 'Uptime : 15:42:13 | Caught 120 pokemon | Visited 220 stops | ' \ + '42.05km walked | Level 25 | Earned 24,069 Stardust | ' \ + '87,500 / 150,000 XP (58%) | 1,337 XP/h | Threw 145 pokeballs | ' \ + 'Highest CP pokemon : highest_cp | Level 25 (87,500 / 150,000, 58%) | ' \ + '+424,242 XP | Encountered 3 new pokemon | ' \ + 'Most perfect pokemon : most_perfect | ' \ 'Encountered 130 pokemon, 120 caught, 30 released, 12 evolved, ' \ '3 never seen before | Released 30 pokemon' From dee28d9fb1a973b95019bc07fb1e206872bd986d Mon Sep 17 00:00:00 2001 From: Eli White Date: Sun, 7 Aug 2016 15:58:16 -0700 Subject: [PATCH 060/202] Rename load_path to load_plugin (#2947) --- pokecli.py | 2 +- pokemongo_bot/plugin_loader.py | 2 +- pokemongo_bot/test/plugin_loader_test.py | 4 ++-- tests/tree_config_builder_test.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pokecli.py b/pokecli.py index 24a0f38ee3..a77cf378c9 100644 --- a/pokecli.py +++ b/pokecli.py @@ -446,7 +446,7 @@ def task_configuration_error(flag_name): plugin_loader = PluginLoader() for plugin in config.plugins: - plugin_loader.load_path(plugin) + plugin_loader.load_plugin(plugin) # create web dir if not exists try: diff --git a/pokemongo_bot/plugin_loader.py b/pokemongo_bot/plugin_loader.py index 3bded030b3..9362197bc8 100644 --- a/pokemongo_bot/plugin_loader.py +++ b/pokemongo_bot/plugin_loader.py @@ -15,7 +15,7 @@ def _get_correct_path(self, path): return correct_path - def load_path(self, path): + def load_plugin(self, path): correct_path = self._get_correct_path(path) if correct_path not in self.folder_cache: diff --git a/pokemongo_bot/test/plugin_loader_test.py b/pokemongo_bot/test/plugin_loader_test.py index 4d4d5ca952..38a566b35c 100644 --- a/pokemongo_bot/test/plugin_loader_test.py +++ b/pokemongo_bot/test/plugin_loader_test.py @@ -15,14 +15,14 @@ def setUp(self): def test_load_namespace_class(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') - self.plugin_loader.load_path(package_path) + self.plugin_loader.load_plugin(package_path) loaded_class = self.plugin_loader.get_class('plugin_fixture.FakeTask') self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') self.plugin_loader.remove_path(package_path) def test_load_zip(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture_test.zip') - self.plugin_loader.load_path(package_path) + self.plugin_loader.load_plugin(package_path) loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') self.plugin_loader.remove_path(package_path) diff --git a/tests/tree_config_builder_test.py b/tests/tree_config_builder_test.py index cd8bed22e0..1992c8187c 100644 --- a/tests/tree_config_builder_test.py +++ b/tests/tree_config_builder_test.py @@ -89,7 +89,7 @@ def test_task_with_config(self): def test_load_plugin_task(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') plugin_loader = PluginLoader() - plugin_loader.load_path(package_path) + plugin_loader.load_plugin(package_path) obj = convert_from_json("""[{ "type": "plugin_fixture.FakeTask" @@ -103,7 +103,7 @@ def test_load_plugin_task(self): def setupUnsupportedBuilder(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'pokemongo_bot', 'test', 'resources', 'plugin_fixture') plugin_loader = PluginLoader() - plugin_loader.load_path(package_path) + plugin_loader.load_plugin(package_path) obj = convert_from_json("""[{ "type": "plugin_fixture.UnsupportedApiTask" From 0855dac7c34cc46b69af9bd4e708e6d172498b31 Mon Sep 17 00:00:00 2001 From: Eli White Date: Sun, 7 Aug 2016 18:08:21 -0700 Subject: [PATCH 061/202] Adding some logic for pulling plugins from github (#2967) --- pokemongo_bot/plugin_loader.py | 57 +++++++++++++++++++++++- pokemongo_bot/plugins/.keep | 1 + pokemongo_bot/test/plugin_loader_test.py | 26 ++++++++++- 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 pokemongo_bot/plugins/.keep diff --git a/pokemongo_bot/plugin_loader.py b/pokemongo_bot/plugin_loader.py index 9362197bc8..8666b53254 100644 --- a/pokemongo_bot/plugin_loader.py +++ b/pokemongo_bot/plugin_loader.py @@ -1,6 +1,8 @@ import os import sys import importlib +import re +import requests class PluginLoader(object): folder_cache = [] @@ -15,8 +17,8 @@ def _get_correct_path(self, path): return correct_path - def load_plugin(self, path): - correct_path = self._get_correct_path(path) + def load_plugin(self, plugin): + correct_path = self._get_correct_path(plugin) if correct_path not in self.folder_cache: self.folder_cache.append(correct_path) @@ -31,3 +33,54 @@ def get_class(self, namespace_class): my_module = importlib.import_module(namespace) return getattr(my_module, class_name) +class GithubPlugin(object): + def __init__(self, plugin_name): + self.plugin_name = plugin_name + self.plugin_parts = self.get_github_parts() + + def is_valid_plugin(self): + return self.plugin_parts is not None + + def get_github_parts(self): + groups = re.match('(.*)\/(.*)#(.*)', self.plugin_name) + + if groups is None: + return None + + parts = {} + parts['user'] = groups.group(1) + parts['repo'] = groups.group(2) + parts['sha'] = groups.group(3) + + return parts + + def get_local_destination(self): + parts = self.plugin_parts + if parts is None: + raise Exception('Not a valid github plugin') + + file_name = '{}_{}_{}.zip'.format(parts['user'], parts['repo'], parts['sha']) + full_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'plugins', file_name) + return full_path + + def get_github_download_url(self): + parts = self.plugin_parts + if parts is None: + raise Exception('Not a valid github plugin') + + github_url = 'https://github.com/{}/{}/archive/{}.zip'.format(parts['user'], parts['repo'], parts['sha']) + return github_url + + def download(self): + url = self.get_github_download_url() + dest = self.get_local_destination() + + r = requests.get(url, stream=True) + r.raise_for_status() + + with open(dest, 'wb') as f: + for chunk in r.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) + r.close() + return dest diff --git a/pokemongo_bot/plugins/.keep b/pokemongo_bot/plugins/.keep new file mode 100644 index 0000000000..5d848b8301 --- /dev/null +++ b/pokemongo_bot/plugins/.keep @@ -0,0 +1 @@ +keep this so we can install plugins into this folder diff --git a/pokemongo_bot/test/plugin_loader_test.py b/pokemongo_bot/test/plugin_loader_test.py index 38a566b35c..ecf50888e6 100644 --- a/pokemongo_bot/test/plugin_loader_test.py +++ b/pokemongo_bot/test/plugin_loader_test.py @@ -6,7 +6,7 @@ import os from datetime import timedelta, datetime from mock import patch, MagicMock -from pokemongo_bot.plugin_loader import PluginLoader +from pokemongo_bot.plugin_loader import PluginLoader, GithubPlugin from pokemongo_bot.test.resources.plugin_fixture import FakeTask class PluginLoaderTest(unittest.TestCase): @@ -26,3 +26,27 @@ def test_load_zip(self): loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') self.plugin_loader.remove_path(package_path) + + def test_get_github_parts_for_valid_github(self): + github_plugin = GithubPlugin('org/repo#sha') + self.assertTrue(github_plugin.is_valid_plugin()) + self.assertEqual(github_plugin.plugin_parts['user'], 'org') + self.assertEqual(github_plugin.plugin_parts['repo'], 'repo') + self.assertEqual(github_plugin.plugin_parts['sha'], 'sha') + + def test_get_github_parts_for_invalid_github(self): + self.assertFalse(GithubPlugin('org/repo').is_valid_plugin()) + self.assertFalse(GithubPlugin('foo').is_valid_plugin()) + self.assertFalse(GithubPlugin('/Users/foo/bar.zip').is_valid_plugin()) + + def test_get_local_destination(self): + github_plugin = GithubPlugin('org/repo#sha') + path = github_plugin.get_local_destination() + expected = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins', 'org_repo_sha.zip')) + self.assertEqual(path, expected) + + def test_get_github_download_url(self): + github_plugin = GithubPlugin('org/repo#sha') + url = github_plugin.get_github_download_url() + expected = 'https://github.com/org/repo/archive/sha.zip' + self.assertEqual(url, expected) From a1733b92b9cb4bd5a864ec506e7709d14d56674b Mon Sep 17 00:00:00 2001 From: mhdasding Date: Mon, 8 Aug 2016 05:16:25 +0200 Subject: [PATCH 062/202] flush after title update (#2977) --- pokemongo_bot/cell_workers/update_title_stats.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index 43e55c260f..acbfaa7fe4 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -111,8 +111,10 @@ def _update_title(self, title, platform): """ if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) + stdout.flush() elif platform == "darwin": stdout.write("\033]0;{}\007".format(title)) + stdout.flush() elif platform == "win32": ctypes.windll.kernel32.SetConsoleTitleA(title) else: From e66c50951c1c0e44a21253d124c82a1bc78ae449 Mon Sep 17 00:00:00 2001 From: rbignon Date: Mon, 8 Aug 2016 05:17:05 +0200 Subject: [PATCH 063/202] correctly re-raise exception to keep backtrace (#2944) --- pokecli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokecli.py b/pokecli.py index a77cf378c9..e00389e06e 100644 --- a/pokecli.py +++ b/pokecli.py @@ -53,7 +53,7 @@ def main(): bot = False - + try: logger.info('PokemonGO Bot v1.0') sys.stdout = codecs.getwriter('utf8')(sys.stdout) @@ -130,7 +130,7 @@ def main(): if bot: report_summary(bot) - raise e + raise def report_summary(bot): if bot.metrics.start_time is None: From bdcf2519d63555ac32246439fd17672896ee64e4 Mon Sep 17 00:00:00 2001 From: Chris Le Date: Sun, 7 Aug 2016 20:17:50 -0700 Subject: [PATCH 064/202] Update MoveToMapPokemon to use events instead of logger. (#2913) --- .gitignore | 5 + pokemongo_bot/__init__.py | 29 +++ .../cell_workers/move_to_map_pokemon.py | 231 +++++++++++++++--- pokemongo_bot/health_record/bot_event.py | 2 +- 4 files changed, 233 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index a12509c322..70ab34b250 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,9 @@ var/ .pydevproject .settings/ +# Cloud9 Users +.c9/ + # Installer logs pip-log.txt pip-delete-this-directory.txt @@ -86,6 +89,8 @@ celerybeat-schedule # virtualenv venv/ ENV/ +local/ +share/ # Spyder project settings .spyderproject diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index d0f034f110..1378e5bdb9 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -393,6 +393,35 @@ def _register_events(self): ) self.event_manager.register_event('unset_pokemon_nickname') + # Move To map pokemon + self.event_manager.register_event( + 'move_to_map_pokemon_fail', + parameters=('message',) + ) + self.event_manager.register_event( + 'move_to_map_pokemon_updated_map', + parameters=('lat', 'lon') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_teleport_to', + parameters=('poke_name', 'poke_dist', 'poke_lat', 'poke_lon', + 'disappears_in') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_encounter', + parameters=('poke_name', 'poke_dist', 'poke_lat', 'poke_lon', + 'disappears_in') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_move_towards', + parameters=('poke_name', 'poke_dist', 'poke_lat', 'poke_lon', + 'disappears_in') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_teleport_back', + parameters=('last_lat', 'last_lon') + ) + def tick(self): self.health_record.heartbeat() self.cell = self.get_meta_cell() diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 08ff35f281..975a9b5a36 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -1,11 +1,59 @@ # -*- coding: utf-8 -*- +""" +Moves a trainer to a Pokemon. + +Events: + move_to_map_pokemon_fail + When the worker fails. + Returns: + message: Failure message. + + move_to_map_pokemon_updated_map + When worker updates the PokemonGo-Map. + Returns: + lat: Latitude + lon: Longitude + + move_to_map_pokemon_teleport_to + When trainer is teleported to a Pokemon. + Returns: + poke_name: Pokemon's name + poke_dist: Distance from the trainer + poke_lat: Latitude of the Pokemon + poke_lon: Longitude of the Pokemon + disappears_in: Number of seconds before the Pokemon disappears + + move_to_map_pokemon_encounter + When a trainer encounters a Pokemon by teleporting or walking. + Returns: + poke_name: Pokemon's name + poke_dist: Distance from the trainer + poke_lat: Latitude of the Pokemon + poke_lon: Longitude of the Pokemon + disappears_in: Number of seconds before the Pokemon disappears + + move_to_map_pokemon_move_towards + When a trainer moves toward a Pokemon. + Returns: + poke_name: Pokemon's name + poke_dist: Distance from the trainer + poke_lat: Latitude of the Pokemon + poke_lon: Longitude of the Pokemon + disappears_in: Number of seconds before the Pokemon disappears + + move_to_map_pokemon_teleport_back + When a trainer teleports back to thier previous location. + Returns: + last_lat: Trainer's last known latitude + last_lon: Trainer's last known longitude + +""" import os import time import json import base64 import requests -from pokemongo_bot import logger from pokemongo_bot.cell_workers.utils import distance, format_dist, format_time from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult @@ -13,7 +61,20 @@ from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker +# Update the map if more than N meters away from the center. (AND'd with +# UPDATE_MAP_MIN_TIME_MINUTES) +UPDATE_MAP_MIN_DISTANCE_METERS = 500 + +# Update the map if it hasn't been updated in n seconds. (AND'd with +# UPDATE_MAP_MIN_DISTANCE_METERS) +UPDATE_MAP_MIN_TIME_SEC = 120 + +# Number of seconds to sleep between teleporting to a snipped Pokemon. +SNIPE_SLEEP_SEC = 2 + + class MoveToMapPokemon(BaseTask): + """Task for moving a trainer to a Pokemon.""" SUPPORTED_TASK_API_VERSION = 1 def initialize(self): @@ -32,13 +93,15 @@ def get_pokemon_from_map(self): try: req = requests.get('{}/raw_data?gyms=false&scanned=false'.format(self.config['address'])) except requests.exceptions.ConnectionError: - logger.log('Could not reach PokemonGo-Map Server', 'red') + self._emit_failure('Could not get Pokemon data from PokemonGo-Map: ' + '{}. Is it running?'.format( + self.config['address'])) return [] try: raw_data = req.json() except ValueError: - logger.log('Map data was not valid', 'red') + self._emit_failure('Map data was not valid') return [] pokemon_list = [] @@ -48,7 +111,7 @@ def get_pokemon_from_map(self): try: pokemon['encounter_id'] = long(base64.b64decode(pokemon['encounter_id'])) except TypeError: - log.logger('base64 error: {}'.format(pokemon['encounter_id']), 'red') + self._emit_failure('base64 error: {}'.format(pokemon['encounter_id'])) continue pokemon['spawn_point_id'] = pokemon['spawnpoint_id'] pokemon['disappear_time'] = int(pokemon['disappear_time'] / 1000) @@ -100,14 +163,17 @@ def update_map_location(self): try: req = requests.get('{}/loc'.format(self.config['address'])) except requests.exceptions.ConnectionError: - logger.log('Could not reach PokemonGo-Map Server', 'red') + self._emit_failure('Could not update trainer location ' + 'PokemonGo-Map: {}. Is it running?'.format( + self.config['address'])) return try: loc_json = req.json() except ValueError: - return log.logger('Map location data was not valid', 'red') - + err = 'Map location data was not valid' + self._emit_failure(err) + return log.logger(err, 'red') dist = distance( self.bot.position[0], @@ -118,32 +184,38 @@ def update_map_location(self): # update map when 500m away from center and last update longer than 2 minutes away now = int(time.time()) - if dist > 500 and now - self.last_map_update > 2 * 60: - requests.post('{}/next_loc?lat={}&lon={}'.format(self.config['address'], self.bot.position[0], self.bot.position[1])) - logger.log('Updated PokemonGo-Map position') + if (dist > UPDATE_MAP_MIN_DISTANCE_METERS and + now - self.last_map_update > UPDATE_MAP_MIN_TIME_SEC): + requests.post( + '{}/next_loc?lat={}&lon={}'.format(self.config['address'], + self.bot.position[0], + self.bot.position[1])) + self.emit_event( + 'move_to_map_pokemon_updated_map', + formatted='Updated PokemonGo-Map to {lat}, {lon}', + data={ + 'lat': self.bot.position[0], + 'lon': self.bot.position[1] + } + ) self.last_map_update = now def snipe(self, pokemon): - last_position = self.bot.position[0:2] - + """Snipe a Pokemon by teleporting. + + Args: + pokemon: Pokemon to snipe. + """ self.bot.heartbeat() - - logger.log('Teleporting to {} ({})'.format(pokemon['name'], format_dist(pokemon['dist'], self.unit)), 'green') - self.bot.api.set_position(pokemon['latitude'], pokemon['longitude'], 0) - - logger.log('Encounter pokemon', 'green') + self._teleport_to(pokemon) catch_worker = PokemonCatchWorker(pokemon, self.bot) api_encounter_response = catch_worker.create_encounter_api_call() - - time.sleep(2) - logger.log('Teleporting back to previous location..', 'green') - self.bot.api.set_position(last_position[0], last_position[1], 0) - time.sleep(2) + time.sleep(SNIPE_SLEEP_SEC) + self._teleport_back() + time.sleep(SNIPE_SLEEP_SEC) self.bot.heartbeat() - catch_worker.work(api_encounter_response) self.add_caught(pokemon) - return WorkerResult.SUCCESS def dump_caught_pokemon(self): @@ -182,18 +254,111 @@ def work(self): if self.config['snipe']: return self.snipe(pokemon) + step_walker = self._move_to(pokemon) + if not step_walker.step(): + return WorkerResult.RUNNING + self._encountered(pokemon) + self.add_caught(pokemon) + return WorkerResult.SUCCESS + + def _emit_failure(self, msg): + """Emits failure to event log. + + Args: + msg: Message to emit + """ + self.emit_event( + 'move_to_map_pokemon_fail', + formatted='Failure! {message}', + data={'message': msg} + ) + + def _emit_log(self, msg): + """Emits log to event log. + + Args: + msg: Message to emit + """ + self.emit_event( + 'move_to_map_pokemon', + formatted='{message}', + data={'message': msg} + ) + + def _pokemon_event_data(self, pokemon): + """Generates parameters used for the Bot's event manager. + + Args: + pokemon: Pokemon object + + Returns: + Dictionary with Pokemon's info. + """ + now = int(time.time()) + return { + 'poke_name': pokemon['name'], + 'poke_dist': (format_dist(pokemon['dist'], self.unit)), + 'poke_lat': pokemon['latitude'], + 'poke_lon': pokemon['longitude'], + 'disappears_in': (format_time(pokemon['disappear_time'] - now)) + } + + def _teleport_to(self, pokemon): + """Teleports trainer to a Pokemon. + + Args: + pokemon: Pokemon to teleport to. + """ + self.emit_event( + 'move_to_map_pokemon_teleport_to', + formatted='Teleporting to {poke_name}. ({poke_dist})', + data=self._pokemon_event_data(pokemon) + ) + self.bot.api.set_position(pokemon['latitude'], pokemon['longitude'], 0) + self._encountered(pokemon) + + def _encountered(self, pokemon): + """Emit event when trainer encounters a Pokemon. + + Args: + pokemon: Pokemon encountered. + """ + self.emit_event( + 'move_to_map_pokemon_encounter', + formatted='Encountered Pokemon: {poke_name}', + data=self._pokemon_event_data(pokemon) + ) + + def _teleport_back(self): + """Teleports trainer back to their last position.""" + last_position = self.bot.position[0:2] + self.emit_event( + 'move_to_map_pokemon_teleport_back', + formatted=('Teleporting back to previous location ({last_lat}, ' + '{last_long})'), + data={'last_lat': last_position[0], 'last_lon': last_position[1]} + ) + self.bot.api.set_position(last_position[0], last_position[1], 0) + + def _move_to(self, pokemon): + """Moves trainer towards a Pokemon. + + Args: + pokemon: Pokemon to move to. + + Returns: + StepWalker + """ now = int(time.time()) - logger.log('Moving towards {}, {} left ({})'.format(pokemon['name'], format_dist(pokemon['dist'], self.unit), format_time(pokemon['disappear_time'] - now))) - step_walker = StepWalker( + self.emit_event( + 'move_to_map_pokemon_move_towards', + formatted=('Moving towards {poke_name}, {poke_dist}, left (' + '{disappears_in})'), + data=self._pokemon_event_data(pokemon) + ) + return StepWalker( self.bot, self.bot.config.walk, pokemon['latitude'], pokemon['longitude'] ) - - if not step_walker.step(): - return WorkerResult.RUNNING - - logger.log('Arrived at {}'.format(pokemon['name'])) - self.add_caught(pokemon) - return WorkerResult.SUCCESS diff --git a/pokemongo_bot/health_record/bot_event.py b/pokemongo_bot/health_record/bot_event.py index f357a4e8e7..55a726049d 100644 --- a/pokemongo_bot/health_record/bot_event.py +++ b/pokemongo_bot/health_record/bot_event.py @@ -16,7 +16,7 @@ def __init__(self, config): # UniversalAnalytics can be reviewed here: # https://github.com/analytics-pros/universal-analytics-python if self.config.health_record: - self.logger.info('Health check is enabled. For more logrmation:') + self.logger.info('Health check is enabled. For more information:') self.logger.info('https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev#analytics') self.client = Client( dsn='https://8abac56480f34b998813d831de262514:196ae1d8dced41099f8253ea2c8fe8e6@app.getsentry.com/90254', From 95902d6ceac7b26da9bade1ce193d066f67c6c04 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 7 Aug 2016 23:59:50 -0400 Subject: [PATCH 065/202] Config/encrypt.so (#2964) * Add config option for libencrypt.so * Correctly set the config value and check for the file in said dir --- configs/config.json.example | 1 + pokecli.py | 1 + pokemongo_bot/__init__.py | 14 ++++++++------ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/configs/config.json.example b/configs/config.json.example index 20ef72e34e..ec46c15eb9 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/pokecli.py b/pokecli.py index e00389e06e..7cc5b5ebf1 100644 --- a/pokecli.py +++ b/pokecli.py @@ -384,6 +384,7 @@ def init_config(): if not config.password and 'password' not in load: config.password = getpass("Password: ") + config.encrypt_location = load.get('encrypt_location','') config.catch = load.get('catch', {}) config.release = load.get('release', {}) config.action_wait_max = load.get('action_wait_max', 4) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 1378e5bdb9..78498e37b0 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -636,7 +636,6 @@ def login(self): ) def get_encryption_lib(self): - file_name = '' if _platform == "linux" or _platform == "linux2" or _platform == "darwin": file_name = 'encrypt.so' elif _platform == "Windows" or _platform == "win32": @@ -646,15 +645,18 @@ def get_encryption_lib(self): else: file_name = 'encrypt.dll' - path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) - full_path = path + '/'+ file_name + if self.config.encrypt_location == '': + path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) + else: + path = self.config.encrypt_location + full_path = path + '/'+ file_name if not os.path.isfile(full_path): - self.logger.error(file_name + ' is not found! Please place it in the bots root directory.') - self.logger.info('Platform: '+ _platform + ' Bot root directory: '+ path) + self.logger.error(file_name + ' is not found! Please place it in the bots root directory or set libencrypt_location in config.') + self.logger.info('Platform: '+ _platform + ' Encrypt.so directory: '+ path) sys.exit(1) else: - self.logger.info('Found '+ file_name +'! Platform: ' + _platform + ' Bot root directory: ' + path) + self.logger.info('Found '+ file_name +'! Platform: ' + _platform + ' Encrypt.so directory: ' + path) return full_path From 41ed10cc9fd0e9a96d76a160095087617f32e849 Mon Sep 17 00:00:00 2001 From: middleagedman Date: Mon, 8 Aug 2016 00:27:13 -0400 Subject: [PATCH 066/202] Fixed mispelling for "formatted" variable (#2984) --- pokecli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokecli.py b/pokecli.py index 7cc5b5ebf1..d428026276 100644 --- a/pokecli.py +++ b/pokecli.py @@ -104,7 +104,7 @@ def main(): 'api_error', sender=bot, level='info', - formmated='Log logged in, reconnecting in {:s}'.format(wait_time) + formatted='Log logged in, reconnecting in {:s}'.format(wait_time) ) time.sleep(wait_time) except ServerBusyOrOfflineException: From 563f898f61233ff86d38d83b68d8399c1f63051b Mon Sep 17 00:00:00 2001 From: Eli White Date: Sun, 7 Aug 2016 21:32:53 -0700 Subject: [PATCH 067/202] Loading plugins from Github (#2992) * Checking github plugin file existence * Loading plugins from github --- pokemongo_bot/plugin_loader.py | 14 ++++++++- pokemongo_bot/test/plugin_loader_test.py | 37 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/pokemongo_bot/plugin_loader.py b/pokemongo_bot/plugin_loader.py index 8666b53254..7a838bb209 100644 --- a/pokemongo_bot/plugin_loader.py +++ b/pokemongo_bot/plugin_loader.py @@ -18,7 +18,14 @@ def _get_correct_path(self, path): return correct_path def load_plugin(self, plugin): - correct_path = self._get_correct_path(plugin) + github_plugin = GithubPlugin(plugin) + if github_plugin.is_valid_plugin(): + if not github_plugin.is_already_downloaded(): + github_plugin.download() + + correct_path = github_plugin.get_local_destination() + else: + correct_path = self._get_correct_path(plugin) if correct_path not in self.folder_cache: self.folder_cache.append(correct_path) @@ -27,6 +34,7 @@ def load_plugin(self, plugin): def remove_path(self, path): correct_path = self._get_correct_path(path) sys.path.remove(correct_path) + self.folder_cache.remove(correct_path) def get_class(self, namespace_class): [namespace, class_name] = namespace_class.split('.') @@ -63,6 +71,10 @@ def get_local_destination(self): full_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'plugins', file_name) return full_path + def is_already_downloaded(self): + file_path = self.get_local_destination() + return os.path.isfile(file_path) + def get_github_download_url(self): parts = self.plugin_parts if parts is None: diff --git a/pokemongo_bot/test/plugin_loader_test.py b/pokemongo_bot/test/plugin_loader_test.py index ecf50888e6..0b0f7da9d1 100644 --- a/pokemongo_bot/test/plugin_loader_test.py +++ b/pokemongo_bot/test/plugin_loader_test.py @@ -4,6 +4,8 @@ import importlib import unittest import os +import shutil +import mock from datetime import timedelta, datetime from mock import patch, MagicMock from pokemongo_bot.plugin_loader import PluginLoader, GithubPlugin @@ -27,6 +29,30 @@ def test_load_zip(self): self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') self.plugin_loader.remove_path(package_path) + def copy_zip(self): + zip_fixture = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture_test.zip') + dest_path = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins', 'org_repo_sha.zip')) + shutil.copyfile(zip_fixture, dest_path) + return dest_path + + def test_load_github_already_downloaded(self): + dest_path = self.copy_zip() + self.plugin_loader.load_plugin('org/repo#sha') + loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + self.plugin_loader.remove_path(dest_path) + os.remove(dest_path) + + @mock.patch.object(GithubPlugin, 'download', copy_zip) + def test_load_github_not_downloaded(self): + self.plugin_loader.load_plugin('org/repo#sha') + loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + dest_path = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins', 'org_repo_sha.zip')) + self.plugin_loader.remove_path(dest_path) + os.remove(dest_path) + +class GithubPluginTest(unittest.TestCase): def test_get_github_parts_for_valid_github(self): github_plugin = GithubPlugin('org/repo#sha') self.assertTrue(github_plugin.is_valid_plugin()) @@ -50,3 +76,14 @@ def test_get_github_download_url(self): url = github_plugin.get_github_download_url() expected = 'https://github.com/org/repo/archive/sha.zip' self.assertEqual(url, expected) + + def test_is_already_downloaded_not_downloaded(self): + github_plugin = GithubPlugin('org/repo#sha') + self.assertFalse(github_plugin.is_already_downloaded()) + + def test_is_already_downloaded_downloaded(self): + github_plugin = GithubPlugin('org/repo#sha') + dest = github_plugin.get_local_destination() + open(dest, 'a').close() + self.assertTrue(github_plugin.is_already_downloaded()) + os.remove(dest) From 229381c318c2f230257dc56f51cd0fd5213ee2d2 Mon Sep 17 00:00:00 2001 From: raulgbcr Date: Mon, 8 Aug 2016 14:12:25 +0200 Subject: [PATCH 068/202] Fixed #3000 (#3003) Fixed syntax error on "move_to_map_pokemon.py" that makes the client crash when using this feature. --- pokemongo_bot/cell_workers/move_to_map_pokemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 975a9b5a36..ec3c8bf89c 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -335,7 +335,7 @@ def _teleport_back(self): self.emit_event( 'move_to_map_pokemon_teleport_back', formatted=('Teleporting back to previous location ({last_lat}, ' - '{last_long})'), + '{last_lon})'), data={'last_lat': last_position[0], 'last_lon': last_position[1]} ) self.bot.api.set_position(last_position[0], last_position[1], 0) From 1a18b9f3b34f1f75bfae9c29ea2985aa3d59653b Mon Sep 17 00:00:00 2001 From: Jaap Moolenaar Date: Mon, 8 Aug 2016 14:13:07 +0200 Subject: [PATCH 069/202] Added MaxPotion inventory count to summary. (#3015) Short Description: The Max Potion count was missing from the inventory summary. Was #2456 --- pokemongo_bot/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 78498e37b0..788e20e721 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -747,7 +747,8 @@ def _print_character_info(self): self.logger.info( 'Potion: ' + str(items_stock[101]) + ' | SuperPotion: ' + str(items_stock[102]) + - ' | HyperPotion: ' + str(items_stock[103])) + ' | HyperPotion: ' + str(items_stock[103]) + + ' | MaxPotion: ' + str(items_stock[104])) self.logger.info( 'Incense: ' + str(items_stock[401]) + From 4faf9624519e1ea9513e3f3c887435fb81c56ab0 Mon Sep 17 00:00:00 2001 From: Arthur Caranta Date: Mon, 8 Aug 2016 14:13:32 +0200 Subject: [PATCH 070/202] Added cleanup of download and files for encrypt.so after they are no longer needed (#3011) --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index dce398c63e..456ae4fb5a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,8 @@ RUN cd /tmp && wget "http://pgoapi.com/pgoencrypt.tar.gz" \ && cd pgoencrypt/src \ && make \ && cp libencrypt.so /usr/src/app/encrypt.so + && cd /tmp + && rm -rf /tmp/pgoencrypt* VOLUME ["/usr/src/app/web"] From 7cc524ed80a918170efcd782829ed6a94e2c4674 Mon Sep 17 00:00:00 2001 From: Jeremy Bi Date: Mon, 8 Aug 2016 20:16:38 +0800 Subject: [PATCH 071/202] Fix bot not returning back after telepoting (#3014) * Fix typo: last_long -> last_lon * Whitespace cleanup * Fix bug introduced by #3037: bot not returning back --- .../cell_workers/move_to_map_pokemon.py | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index ec3c8bf89c..9ab6bbb7a2 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -7,13 +7,13 @@ When the worker fails. Returns: message: Failure message. - + move_to_map_pokemon_updated_map When worker updates the PokemonGo-Map. Returns: lat: Latitude lon: Longitude - + move_to_map_pokemon_teleport_to When trainer is teleported to a Pokemon. Returns: @@ -22,7 +22,7 @@ poke_lat: Latitude of the Pokemon poke_lon: Longitude of the Pokemon disappears_in: Number of seconds before the Pokemon disappears - + move_to_map_pokemon_encounter When a trainer encounters a Pokemon by teleporting or walking. Returns: @@ -31,7 +31,7 @@ poke_lat: Latitude of the Pokemon poke_lon: Longitude of the Pokemon disappears_in: Number of seconds before the Pokemon disappears - + move_to_map_pokemon_move_towards When a trainer moves toward a Pokemon. Returns: @@ -40,7 +40,7 @@ poke_lat: Latitude of the Pokemon poke_lon: Longitude of the Pokemon disappears_in: Number of seconds before the Pokemon disappears - + move_to_map_pokemon_teleport_back When a trainer teleports back to thier previous location. Returns: @@ -184,11 +184,11 @@ def update_map_location(self): # update map when 500m away from center and last update longer than 2 minutes away now = int(time.time()) - if (dist > UPDATE_MAP_MIN_DISTANCE_METERS and + if (dist > UPDATE_MAP_MIN_DISTANCE_METERS and now - self.last_map_update > UPDATE_MAP_MIN_TIME_SEC): requests.post( - '{}/next_loc?lat={}&lon={}'.format(self.config['address'], - self.bot.position[0], + '{}/next_loc?lat={}&lon={}'.format(self.config['address'], + self.bot.position[0], self.bot.position[1])) self.emit_event( 'move_to_map_pokemon_updated_map', @@ -202,16 +202,18 @@ def update_map_location(self): def snipe(self, pokemon): """Snipe a Pokemon by teleporting. - + Args: pokemon: Pokemon to snipe. """ + last_position = self.bot.position[0:2] self.bot.heartbeat() self._teleport_to(pokemon) catch_worker = PokemonCatchWorker(pokemon, self.bot) api_encounter_response = catch_worker.create_encounter_api_call() time.sleep(SNIPE_SLEEP_SEC) - self._teleport_back() + self._teleport_back(last_position) + self.bot.api.set_position(last_position[0], last_position[1], 0) time.sleep(SNIPE_SLEEP_SEC) self.bot.heartbeat() catch_worker.work(api_encounter_response) @@ -263,34 +265,34 @@ def work(self): def _emit_failure(self, msg): """Emits failure to event log. - + Args: msg: Message to emit """ self.emit_event( - 'move_to_map_pokemon_fail', + 'move_to_map_pokemon_fail', formatted='Failure! {message}', data={'message': msg} ) - + def _emit_log(self, msg): """Emits log to event log. - + Args: msg: Message to emit """ self.emit_event( - 'move_to_map_pokemon', + 'move_to_map_pokemon', formatted='{message}', data={'message': msg} ) - + def _pokemon_event_data(self, pokemon): """Generates parameters used for the Bot's event manager. - + Args: pokemon: Pokemon object - + Returns: Dictionary with Pokemon's info. """ @@ -298,14 +300,14 @@ def _pokemon_event_data(self, pokemon): return { 'poke_name': pokemon['name'], 'poke_dist': (format_dist(pokemon['dist'], self.unit)), - 'poke_lat': pokemon['latitude'], + 'poke_lat': pokemon['latitude'], 'poke_lon': pokemon['longitude'], 'disappears_in': (format_time(pokemon['disappear_time'] - now)) } - + def _teleport_to(self, pokemon): """Teleports trainer to a Pokemon. - + Args: pokemon: Pokemon to teleport to. """ @@ -316,7 +318,7 @@ def _teleport_to(self, pokemon): ) self.bot.api.set_position(pokemon['latitude'], pokemon['longitude'], 0) self._encountered(pokemon) - + def _encountered(self, pokemon): """Emit event when trainer encounters a Pokemon. @@ -329,23 +331,21 @@ def _encountered(self, pokemon): data=self._pokemon_event_data(pokemon) ) - def _teleport_back(self): - """Teleports trainer back to their last position.""" - last_position = self.bot.position[0:2] + def _teleport_back(self, last_position): + """Teleports trainer back to their last position.""" self.emit_event( 'move_to_map_pokemon_teleport_back', formatted=('Teleporting back to previous location ({last_lat}, ' '{last_lon})'), data={'last_lat': last_position[0], 'last_lon': last_position[1]} ) - self.bot.api.set_position(last_position[0], last_position[1], 0) - + def _move_to(self, pokemon): """Moves trainer towards a Pokemon. - + Args: pokemon: Pokemon to move to. - + Returns: StepWalker """ From 283c17e5dcb58267d895629cfa968aa65cae975d Mon Sep 17 00:00:00 2001 From: Nikos Filippakis Date: Mon, 8 Aug 2016 17:24:24 +0200 Subject: [PATCH 072/202] Fix Dockerfile installation (#3057) --- CONTRIBUTORS.md | 1 + Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ee5aa7063d..6dceaf0918 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -53,3 +53,4 @@ * lucasfevi * Moonlight-Angel * mjmadsen + * nikofil diff --git a/Dockerfile b/Dockerfile index 456ae4fb5a..f98d5d6942 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,8 +11,8 @@ RUN cd /tmp && wget "http://pgoapi.com/pgoencrypt.tar.gz" \ && tar zxvf pgoencrypt.tar.gz \ && cd pgoencrypt/src \ && make \ - && cp libencrypt.so /usr/src/app/encrypt.so - && cd /tmp + && cp libencrypt.so /usr/src/app/encrypt.so \ + && cd /tmp \ && rm -rf /tmp/pgoencrypt* VOLUME ["/usr/src/app/web"] From 351ea76b62a2708cae72d59977b42c6f3f204250 Mon Sep 17 00:00:00 2001 From: cmezh Date: Mon, 8 Aug 2016 22:42:17 +0700 Subject: [PATCH 073/202] Fix for #3045 (#3055) --- pokecli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokecli.py b/pokecli.py index d428026276..65d9923798 100644 --- a/pokecli.py +++ b/pokecli.py @@ -104,7 +104,7 @@ def main(): 'api_error', sender=bot, level='info', - formatted='Log logged in, reconnecting in {:s}'.format(wait_time) + formatted='Log logged in, reconnecting in {:d}'.format(wait_time) ) time.sleep(wait_time) except ServerBusyOrOfflineException: From ae0ae815089bf1e3b17497b476d524a0468949e2 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Mon, 8 Aug 2016 09:21:44 -0700 Subject: [PATCH 074/202] Update README.md (#3090) --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index f269d43e6d..171ee4f355 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,8 @@ The project is currently setup in two main branches. `dev` and `master`. ## Please submit PR to [Dev branch](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) -## Where to get the DLL/SO? A help channel is coming. -You need to grab them from the Internet. - We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) +You can count on the community in #help channel. ## Table of Contents - [Features](#features) From 20aeb90bc0712d4f023c311844f4c1174061d600 Mon Sep 17 00:00:00 2001 From: mjmadsen Date: Mon, 8 Aug 2016 11:22:19 -0500 Subject: [PATCH 075/202] Added request to check configuration (#3089) --- .github/ISSUE_TEMPLATE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 9976991cf9..a1d7168e75 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,5 @@ +Please check configuration at http://jsonlint.com/ before posting an issue. + ### Expected Behavior From ff380cd0e07f225ad1ae2c739bea9f9f80e2f8ce Mon Sep 17 00:00:00 2001 From: middleagedman Date: Mon, 8 Aug 2016 12:49:26 -0400 Subject: [PATCH 076/202] Fixed Dockerfile - missing \ on command lines (#3096) * Fixed mispelling for "formatted" variable * Docker commands missing trailing \ From a5e91315ed82581a901fbf5019a3708e5e7a471b Mon Sep 17 00:00:00 2001 From: Ajurna Date: Mon, 8 Aug 2016 19:34:31 +0100 Subject: [PATCH 077/202] Fix for FileIO slowing bot performance.This puts the map writing into a thread and makes sure it only executes once. (#3100) --- pokemongo_bot/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 788e20e721..67c7f9a6b0 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -9,6 +9,8 @@ import re import sys import time +import Queue +import threading from geopy.geocoders import GoogleV3 from pgoapi import PGoApi @@ -69,6 +71,11 @@ def __init__(self, config): # Make our own copy of the workers for this instance self.workers = [] + # Theading setup for file writing + self.web_update_queue = Queue.Queue(maxsize=1) + self.web_update_thread = threading.Thread(target=self.update_web_location_worker) + self.web_update_thread.start() + def start(self): self._setup_event_system() self._setup_logging() @@ -976,7 +983,15 @@ def heartbeat(self): request.get_player() request.check_awarded_badges() request.call() - self.update_web_location() # updates every tick + try: + self.web_update_queue.put_nowait(True) # do this outside of thread every tick + except Queue.Full: + pass + + def update_web_location_worker(self): + while True: + self.web_update_queue.get() + self.update_web_location() def get_inventory_count(self, what): response_dict = self.get_inventory() From d8546d7bfa660e827c903582da0c7ac714db8406 Mon Sep 17 00:00:00 2001 From: Tushar Saini Date: Mon, 8 Aug 2016 13:39:33 -0500 Subject: [PATCH 078/202] Change word usage: "fled" to "escaped" (#3118) "fled" is confusing to lot of people and is easily confused with pokemon vanishing. "escaped" is a better term. --- pokemongo_bot/cell_workers/pokemon_catch_worker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index d676e9f0e2..d5118b391b 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -324,8 +324,8 @@ def work(self, response_dict=None): 'CATCH_POKEMON']['status'] if status is 2: self.emit_event( - 'pokemon_fled', - formatted="{pokemon} fled.", + 'pokemon_escaped', + formatted="{pokemon} escaped.", data={'pokemon': pokemon_name} ) sleep(2) From 0b3aa4f5fcb6d353e062019c2b482b6a5f2631fc Mon Sep 17 00:00:00 2001 From: HKLCF Date: Tue, 9 Aug 2016 02:40:41 +0800 Subject: [PATCH 079/202] Update the example config file (#3120) * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Rename path.example.json to path.json.example --- configs/config.json.cluster.example | 1 + configs/config.json.map.example | 1 + configs/config.json.path.example | 1 + configs/config.json.pokemon.example | 1 + configs/{path.example.json => path.json.example} | 0 5 files changed, 4 insertions(+) rename configs/{path.example.json => path.json.example} (100%) diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index 8d0d8f854f..b32eb4f668 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.map.example b/configs/config.json.map.example index e665d4c6da..1079c999f9 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.path.example b/configs/config.json.path.example index afd1e3afeb..94a9fdba07 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 7cad1ac066..1d428a6ae7 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/path.example.json b/configs/path.json.example similarity index 100% rename from configs/path.example.json rename to configs/path.json.example From d0f60a221a8af7eaf7b41ffeeb756a636032534e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kay=20G=C3=BCrcan?= Date: Mon, 8 Aug 2016 21:44:49 +0300 Subject: [PATCH 080/202] typo: logrmation -> information (#2601) Fix a typo. I assume that it was "information" initially, but became "logrmation" when someone used replace all functionality to replace all infos with logs. But I might be totally wrong at this point, idk. Just didn't like the word and wanted to fix that typo. From f648be31c15d0143bccc0531571b2d00fd88ec4d Mon Sep 17 00:00:00 2001 From: pmquan Date: Mon, 8 Aug 2016 12:08:50 -0700 Subject: [PATCH 081/202] Change fled to escaped (#3129) Fix an issue after PR #3118 --- pokemongo_bot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 67c7f9a6b0..8e9a28ce3a 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -256,7 +256,7 @@ def _register_events(self): ) ) self.event_manager.register_event( - 'pokemon_fled', + 'pokemon_escaped', parameters=('pokemon',) ) self.event_manager.register_event( From 47ab81f5e1e34a36a33cf9c46949b5e51ab6e412 Mon Sep 17 00:00:00 2001 From: Chris Wild Date: Mon, 8 Aug 2016 21:07:09 +0100 Subject: [PATCH 082/202] When JSON parsing fails, give a rough indication of why (#3137) * When JSON parsing fails, give a rough indication of why * Use the official package instead of SHA1 commit --- pokecli.py | 26 ++++++++++++++++++++++---- requirements.txt | 1 + 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/pokecli.py b/pokecli.py index 65d9923798..55afa55399 100644 --- a/pokecli.py +++ b/pokecli.py @@ -42,6 +42,12 @@ from pokemongo_bot.health_record import BotEvent from pokemongo_bot.plugin_loader import PluginLoader +try: + from demjson import jsonlint +except ImportError: + # Run `pip install -r requirements.txt` to fix this + jsonlint = None + if sys.version_info >= (2, 7, 9): ssl._create_default_https_context = ssl._create_unverified_context @@ -162,16 +168,28 @@ def init_config(): # If config file exists, load variables from json load = {} + def _json_loader(filename): + try: + with open(filename, 'rb') as data: + load.update(json.load(data)) + except ValueError: + if jsonlint: + with open(filename, 'rb') as data: + lint = jsonlint() + rc = lint.main(['-v', filename]) + + logger.critical('Error with configuration file') + sys.exit(-1) + # Select a config file code parser.add_argument("-cf", "--config", help="Config File to use") config_arg = parser.parse_known_args() and parser.parse_known_args()[0].config or None + if config_arg and os.path.isfile(config_arg): - with open(config_arg) as data: - load.update(json.load(data)) + _json_loader(config_arg) elif os.path.isfile(config_file): logger.info('No config argument specified, checking for /configs/config.json') - with open(config_file) as data: - load.update(json.load(data)) + _json_loader(config_file) else: logger.info('Error: No /configs/config.json or specified config') diff --git a/requirements.txt b/requirements.txt index aabf40937d..f6a22a0233 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,3 +21,4 @@ gpxpy==1.1.1 mock==2.0.0 timeout-decorator==0.3.2 raven==5.23.0 +demjson==2.2.4 From c8a33bca367a7bd77b499e711ccc4f12142e3e5b Mon Sep 17 00:00:00 2001 From: Eli White Date: Mon, 8 Aug 2016 13:39:24 -0700 Subject: [PATCH 083/202] Handle Github Download Zip Format (#3108) * Checking github plugin file existence * Loading plugins from github * Starting install code for github plugins * Updating GithubPlugin to support extracting folders * Handling github zip formats by extracting to the correct location --- pokemongo_bot/plugin_loader.py | 64 +++++++++-- pokemongo_bot/test/plugin_loader_test.py | 105 ++++++++++++++---- .../test/resources/plugin_fixture_test.zip | Bin 3412 -> 1939 bytes pokemongo_bot/test/resources/plugin_sha/.sha | 1 + ...54eddde33061be9b329efae0cfb9bd58842655.zip | Bin 0 -> 1734 bytes 5 files changed, 140 insertions(+), 30 deletions(-) create mode 100644 pokemongo_bot/test/resources/plugin_sha/.sha create mode 100644 pokemongo_bot/test/resources/test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip diff --git a/pokemongo_bot/plugin_loader.py b/pokemongo_bot/plugin_loader.py index 7a838bb209..f7e12a85a7 100644 --- a/pokemongo_bot/plugin_loader.py +++ b/pokemongo_bot/plugin_loader.py @@ -3,6 +3,8 @@ import importlib import re import requests +import zipfile +import shutil class PluginLoader(object): folder_cache = [] @@ -20,10 +22,11 @@ def _get_correct_path(self, path): def load_plugin(self, plugin): github_plugin = GithubPlugin(plugin) if github_plugin.is_valid_plugin(): - if not github_plugin.is_already_downloaded(): - github_plugin.download() + if not github_plugin.is_already_installed(): + github_plugin.install() + + correct_path = github_plugin.get_plugin_folder() - correct_path = github_plugin.get_local_destination() else: correct_path = self._get_correct_path(plugin) @@ -42,6 +45,8 @@ def get_class(self, namespace_class): return getattr(my_module, class_name) class GithubPlugin(object): + PLUGINS_FOLDER = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'plugins') + def __init__(self, plugin_name): self.plugin_name = plugin_name self.plugin_parts = self.get_github_parts() @@ -62,18 +67,45 @@ def get_github_parts(self): return parts + def get_installed_version(self): + if not self.is_already_installed(): + return None + + filename = os.path.join(self.get_plugin_folder(), '.sha') + print filename + with open(filename) as file: + return file.read().strip() + def get_local_destination(self): parts = self.plugin_parts if parts is None: raise Exception('Not a valid github plugin') file_name = '{}_{}_{}.zip'.format(parts['user'], parts['repo'], parts['sha']) - full_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'plugins', file_name) + full_path = os.path.join(self.PLUGINS_FOLDER, file_name) return full_path - def is_already_downloaded(self): - file_path = self.get_local_destination() - return os.path.isfile(file_path) + def is_already_installed(self): + file_path = self.get_plugin_folder() + if not os.path.isdir(file_path): + return False + + sha_file = os.path.join(file_path, '.sha') + + if not os.path.isfile(sha_file): + return False + + with open(sha_file) as file: + content = file.read().strip() + + if content != self.plugin_parts['sha']: + return False + + return True + + def get_plugin_folder(self): + folder_name = '{}_{}'.format(self.plugin_parts['user'], self.plugin_parts['repo']) + return os.path.join(self.PLUGINS_FOLDER, folder_name) def get_github_download_url(self): parts = self.plugin_parts @@ -83,6 +115,24 @@ def get_github_download_url(self): github_url = 'https://github.com/{}/{}/archive/{}.zip'.format(parts['user'], parts['repo'], parts['sha']) return github_url + def install(self): + self.download() + self.extract() + + def extract(self): + dest = self.get_plugin_folder() + with zipfile.ZipFile(self.get_local_destination(), "r") as z: + z.extractall(dest) + + github_folder = os.path.join(dest, '{}-{}'.format(self.plugin_parts['repo'], self.plugin_parts['sha'])) + new_folder = os.path.join(dest, '{}'.format(self.plugin_parts['repo'])) + shutil.move(github_folder, new_folder) + + with open(os.path.join(dest, '.sha'), 'w') as file: + file.write(self.plugin_parts['sha']) + + os.remove(self.get_local_destination()) + def download(self): url = self.get_github_download_url() dest = self.get_local_destination() diff --git a/pokemongo_bot/test/plugin_loader_test.py b/pokemongo_bot/test/plugin_loader_test.py index 0b0f7da9d1..ed285ede67 100644 --- a/pokemongo_bot/test/plugin_loader_test.py +++ b/pokemongo_bot/test/plugin_loader_test.py @@ -11,6 +11,8 @@ from pokemongo_bot.plugin_loader import PluginLoader, GithubPlugin from pokemongo_bot.test.resources.plugin_fixture import FakeTask +PLUGIN_PATH = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins')) + class PluginLoaderTest(unittest.TestCase): def setUp(self): self.plugin_loader = PluginLoader() @@ -26,31 +28,39 @@ def test_load_zip(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture_test.zip') self.plugin_loader.load_plugin(package_path) loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') - self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTaskZip') self.plugin_loader.remove_path(package_path) - def copy_zip(self): - zip_fixture = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture_test.zip') - dest_path = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins', 'org_repo_sha.zip')) - shutil.copyfile(zip_fixture, dest_path) + def copy_plugin(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') + dest_path = os.path.join(PLUGIN_PATH, 'org_repo', 'plugin_fixture_tests') + shutil.copytree(package_path, os.path.join(dest_path)) + with open(os.path.join(os.path.dirname(dest_path), '.sha'), 'w') as file: + file.write('testsha') return dest_path def test_load_github_already_downloaded(self): - dest_path = self.copy_zip() - self.plugin_loader.load_plugin('org/repo#sha') - loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') + dest_path = self.copy_plugin() + self.plugin_loader.load_plugin('org/repo#testsha') + loaded_class = self.plugin_loader.get_class('plugin_fixture_tests.FakeTask') self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') self.plugin_loader.remove_path(dest_path) - os.remove(dest_path) + shutil.rmtree(os.path.dirname(dest_path)) + + def copy_zip(self): + zip_name = 'test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip' + fixture_zip = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', zip_name) + zip_dest = os.path.join(PLUGIN_PATH, 'org_test-pgo-plugin_2d54eddde33061be9b329efae0cfb9bd58842655.zip') + shutil.copyfile(fixture_zip, zip_dest) @mock.patch.object(GithubPlugin, 'download', copy_zip) def test_load_github_not_downloaded(self): - self.plugin_loader.load_plugin('org/repo#sha') - loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') - self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') - dest_path = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins', 'org_repo_sha.zip')) - self.plugin_loader.remove_path(dest_path) - os.remove(dest_path) + self.plugin_loader.load_plugin('org/test-pgo-plugin#2d54eddde33061be9b329efae0cfb9bd58842655') + loaded_class = self.plugin_loader.get_class('test-pgo-plugin.PrintText') + self.assertEqual(loaded_class({}, {}).work(), 'PrintText') + dest_path = os.path.join(PLUGIN_PATH, 'org_test-pgo-plugin') + self.plugin_loader.remove_path(os.path.join(dest_path, 'test-pgo-plugin')) + shutil.rmtree(dest_path) class GithubPluginTest(unittest.TestCase): def test_get_github_parts_for_valid_github(self): @@ -65,10 +75,25 @@ def test_get_github_parts_for_invalid_github(self): self.assertFalse(GithubPlugin('foo').is_valid_plugin()) self.assertFalse(GithubPlugin('/Users/foo/bar.zip').is_valid_plugin()) + def test_get_installed_version(self): + github_plugin = GithubPlugin('org/repo#my-version') + src_fixture = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_sha') + dest = github_plugin.get_plugin_folder() + shutil.copytree(src_fixture, dest) + actual = github_plugin.get_installed_version() + shutil.rmtree(dest) + self.assertEqual('my-version', actual) + + def test_get_plugin_folder(self): + github_plugin = GithubPlugin('org/repo#sha') + expected = os.path.join(PLUGIN_PATH, 'org_repo') + actual = github_plugin.get_plugin_folder() + self.assertEqual(actual, expected) + def test_get_local_destination(self): github_plugin = GithubPlugin('org/repo#sha') path = github_plugin.get_local_destination() - expected = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins', 'org_repo_sha.zip')) + expected = os.path.join(PLUGIN_PATH, 'org_repo_sha.zip') self.assertEqual(path, expected) def test_get_github_download_url(self): @@ -77,13 +102,47 @@ def test_get_github_download_url(self): expected = 'https://github.com/org/repo/archive/sha.zip' self.assertEqual(url, expected) - def test_is_already_downloaded_not_downloaded(self): + def test_is_already_installed_not_installed(self): + github_plugin = GithubPlugin('org/repo#sha') + self.assertFalse(github_plugin.is_already_installed()) + + def test_is_already_installed_version_mismatch(self): github_plugin = GithubPlugin('org/repo#sha') - self.assertFalse(github_plugin.is_already_downloaded()) + plugin_folder = github_plugin.get_plugin_folder() + os.mkdir(plugin_folder) + with open(os.path.join(plugin_folder, '.sha'), 'w') as file: + file.write('sha2') - def test_is_already_downloaded_downloaded(self): + actual = github_plugin.is_already_installed() + shutil.rmtree(plugin_folder) + self.assertFalse(actual) + + def test_is_already_installed_installed(self): github_plugin = GithubPlugin('org/repo#sha') - dest = github_plugin.get_local_destination() - open(dest, 'a').close() - self.assertTrue(github_plugin.is_already_downloaded()) - os.remove(dest) + plugin_folder = github_plugin.get_plugin_folder() + os.mkdir(plugin_folder) + with open(os.path.join(plugin_folder, '.sha'), 'w') as file: + file.write('sha') + + actual = github_plugin.is_already_installed() + shutil.rmtree(plugin_folder) + self.assertTrue(actual) + + def test_extract(self): + github_plugin = GithubPlugin('org/test-pgo-plugin#2d54eddde33061be9b329efae0cfb9bd58842655') + src = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip') + zip_dest = github_plugin.get_local_destination() + shutil.copyfile(src, zip_dest) + github_plugin.extract() + plugin_folder = github_plugin.get_plugin_folder() + os.path.isdir(plugin_folder) + sub_folder = os.path.join(plugin_folder, 'test-pgo-plugin') + os.path.isdir(sub_folder) + sha_file = os.path.join(github_plugin.get_plugin_folder(), '.sha') + os.path.isfile(sha_file) + + with open(sha_file) as file: + content = file.read().strip() + self.assertEqual(content, '2d54eddde33061be9b329efae0cfb9bd58842655') + + shutil.rmtree(plugin_folder) diff --git a/pokemongo_bot/test/resources/plugin_fixture_test.zip b/pokemongo_bot/test/resources/plugin_fixture_test.zip index 335d95e5226056902bb02114fa26a2b3dad61ca1..78828798c32b46cb56eac6df953e7add47c175cf 100644 GIT binary patch delta 564 zcmca2HJP6$z?+$civa`#w@>8Jl2Daj5iSJ8Ul~OhKyo4s96%_teWIT+y8uvJTzBe z?UTP2rKskudemEWZEnUbYd?z(IW;{;`c(fVsP44FwpL~y9Tq!hyg8^X%D;p?TfTDjV69dB$77z~reUPTy delta 1852 zcmbQte?^KXz?+$civa|hB`5M|Ni>Nq4=?3h9{!b4gaITc!oUH9*^(3ejM+PYa-N*a zCw5$9`lT>Ano(W?Bm%-P)ew__l9L#XF(hF|O}@)$2QzE35>pp*wb=5>>zSgNnk6Um zFsn_T%fwxu92&vH02GJ2C3J$f7mJ}loA%u%egPMortW(gdJ2p5N|tt5b!_I$Ek7$@ zeOEL{^n!4u1OI}lnT|R^@;_Jqvw6M#@7~*Q&p+p@TYPEHg(u0b>km6HNbu_L&RC?) zA=+cAlYQ0H#w!PKT7Ud{i%gXhJ}3-CneNN*1o)(D|TvK($X^Z zsA>)K3CgbjKJPaBwKKXb`+@uq*F^E2PF=SA1-Y9&tvsxub#AZ$Ld+zUtRsO=&js}f~1>2B)a8U!`+N;<}#bJOulBY_=Y|AH+GN< zjqOD|Yk@AD1jLBM(JaaC2^4?>MFxS%A6Y~vvGZU?Ha(dCfU_y# zJCoA3DAyxqDr=MKrkUKFGGmGKrsymAd%M}cFj#HQ?2cRDU3TL3+{*mty}xU3RNwet z<(05E=FapTUp3oA?AcTujw&P_2x3=h?s0r|Nc{l&f#N3)w;C_pKK$hmd;Rp|AI{4p zNF0muOOA~w5Ed89Y1PQ}a5^rbB4B??P*(ZfiB^R#GD%C%w5;{ch!2cpfAjd(oI;1^ zav!(-Dp{R6bE~^ZNlWATEjcFBQ_sD2+k4_j#=F%{VOf={&$XTUu*;QO^;t*Hl{WLB z)uEF;&b}_%dSbfL>HB9^O|&^YujSyf`d?pnht`DpUR2dRUiPBYER17gkG5vZ%gBJ0 zC80t#juPi;>VXr zkcQ=D^yt!oMAu{i7IDn@IvR8v9$yOkIO-e)-u!FmSSu#Dk=Jwejp?en+f>a?>fPjh zwfSGS{0sIMXSRiQ*}a}*`=rIP_?giE+U=#A|CX7zUj6uS-a}J&4oy3rqK2k}GZ?*X zC-9{41j)xS#xcA*F1LiYqgj{de7)TV7M}ZW_?*lQi)PGdz3XnI<-2z4sx>~=R*TQa zPg8j@=_z9qOOaEu*O?QkZ=6Ng<({hFm@s$p8pd^JQhn~TvVIWeS;l5(w8<#undJ4U zRTF}Ydi_P#q@3e)-CQ>TgPkuMlHMQZ^MeEQ*)o>`xm_S6ASG0`%&M$pmM&Bdd;b8 zr*C|=V+6&S>c+C#5MZ2r1IHO7lPEJ60~;t00ZYM4EEZz0f;hk%BnK}$6<8g`pyhBl zL<(N!mb2=Dr9jeXQoEqK5K?dh1=)Pyg>ed7kP@uKLADo^WDup(Z@U diff --git a/pokemongo_bot/test/resources/plugin_sha/.sha b/pokemongo_bot/test/resources/plugin_sha/.sha new file mode 100644 index 0000000000..eaf604c9ac --- /dev/null +++ b/pokemongo_bot/test/resources/plugin_sha/.sha @@ -0,0 +1 @@ +my-version diff --git a/pokemongo_bot/test/resources/test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip b/pokemongo_bot/test/resources/test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip new file mode 100644 index 0000000000000000000000000000000000000000..a692ac3f08492882a28820569a62b59d2647006f GIT binary patch literal 1734 zcmWIWW@h1H00EUv?4Do-l(1yrWGG23F3~MW&(|%;DNWDJ(=|#lHAziLNl7&}HZU_x zO0`TfHnL1jOH4ILPD`>(N-?#tFflSSHPsIdVP#;vD!L*(0BSx5*!)Ruz2?453=EZY+*mHVk{C#!H>m@IgCcmF3;%OWIa(3v7jeA_4XI_~5Oe3#w|LWT6`t|M)BHw;5 z+n=YH5|>_-JvUN%$C}s*HSH>gtm6wN z>+z<~X`LdrR!?om#7$FL-8LR)Hxx0cST=G(^n!Ao*PN!P%_x4c&Yp4}6yT<(=;iumAbz&AQdA zZ&miDu6pyqY{&0vBgJIj^}ix^Xmn;rZ$Gc*vu3l*@sG+0XBOK;KmNYMRbe{M*{KKg z1#UkS3ZHKp;j~)+igT;V&YQQKUzpGKiP7JfyDm@l(VIh&{5k#HtSfmP)6B|$hs%cL z-Vu>b?(h`$WPNXT_7%I>tC{=(VKL78kN18PkUSq&-?GE$%Bs6dE+vRQovzVx^zsr> zAwwqCvnDcs)l9Y;wr1$LcuEumYKHRdP@m2srF(DXKlAzWbtShww4<&X7K&a;&784I zXnjTJpHmb62~TH;U-XXwHQCl5-?K;#m@VXi*oBg08y}yUmst`YuUAkBPq*IMr_P6- zSlgqoaq5hA;E6N3C-k)fIT#9^g0#JMpgF|$*O_^9fsQy&sY7xr;|q#1^Gc8%;@jiT z-Q*y0&rU{@JX(=-uSI>R&%z*2v5=-aj+ac?0i)BdZKc zIh0>MFi`i2ev^10>}y8W6xK`YPRyM&Ww+R@ItTB9c}#|t$4=>$^X Date: Mon, 8 Aug 2016 13:49:24 -0700 Subject: [PATCH 084/202] Dev merge to master, PR (#3146) * Adding plugin support (#2679) * Adding plugin support * Adding an empty __init__.py * Moving the base task to the project root (#2702) * Moving the base task to the project root * Moving the base class more * Changing the import again * Adding a heartbeat to the analytics (#2709) * Adding a heartbeat to the analytics * Heartbeat every 30 seconds, not every 5 * Don't double track clients * Fix 'local variable 'bot' referenced before assignment' * Providing an error if tasks don't work for the given api (#2732) * Fix for utf8 encoding when catching lured pokemon (#2720) * Fixing lure pokestop encoding * fixing lure encoding * Fix For catchable not being displayed on the web (#2719) * Fix For catchable not being displayed on the web * Update catch_visible_pokemon.py * Added encrypt.so compilation process to Dockerfile (#2695) * OS Detection for encrypt lib (#2768) Fix 32bit check, darwin and linux use the same file Make it a function Check if file exists, if not show error Define file_name first Fix return Check if file exists, if not show error Print info about paths Fix for 32/64bit detection * Fix Typo in unexpected_response_retry (#2531) fixes #2525 #2523 * Revert "changing license from MIT to GPLv3" This reverts commit 69fb64f2bf7c12e28c2bb6d2b636c6af55822448. * When the google analytics domain is blocked the bot crashed. (#2764) With a simple try / except this can be solved. Fix dirty catch all * Fixes #2698 - Prevents "Possibly searching too often" error after re-login. (#2771) * Fixes #2698 - Added api.activate_signature call to prevent issue after re-login. - Also replaced deprecated log call with event_manager emit to prevent exception being thrown. * Modified to use OS detected library path as per PR #2768 * Support loading plugins from .zip files (#2766) * Keep track of how many pokemon released (#2884) * Setting Library path to work with encrypt.so (#2899) Setting LD_LIBRARY_PATH on Dockerfile * :sparkles: Added login and username to available stats (#2494) Added a player_data property in PokemonGoBot to access player data from outside Added unit tests for login and username stats Added tests for call args when updating the window title Added a platform-specific test for window title updating on win32 platform * [dev] small fixes (#2912) * Fixed emit_event typo * Update CONTRIBUTORS.md * Changed initialization location for "bot" We use bot in main exception on 128 * Update pokecli.py * Rename load_path to load_plugin (#2947) * Adding some logic for pulling plugins from github (#2967) * flush after title update (#2977) * correctly re-raise exception to keep backtrace (#2944) * Update MoveToMapPokemon to use events instead of logger. (#2913) * Config/encrypt.so (#2964) * Add config option for libencrypt.so * Correctly set the config value and check for the file in said dir * Fixed mispelling for "formatted" variable (#2984) * Loading plugins from Github (#2992) * Checking github plugin file existence * Loading plugins from github * Fixed #3000 (#3003) Fixed syntax error on "move_to_map_pokemon.py" that makes the client crash when using this feature. * Added MaxPotion inventory count to summary. (#3015) Short Description: The Max Potion count was missing from the inventory summary. Was #2456 * Added cleanup of download and files for encrypt.so after they are no longer needed (#3011) * Fix bot not returning back after telepoting (#3014) * Fix typo: last_long -> last_lon * Whitespace cleanup * Fix bug introduced by #3037: bot not returning back * Fix Dockerfile installation (#3057) * Fix for #3045 (#3055) * Added request to check configuration (#3089) * Fixed Dockerfile - missing \ on command lines (#3096) * Fixed mispelling for "formatted" variable * Docker commands missing trailing \ * Fix for FileIO slowing bot performance.This puts the map writing into a thread and makes sure it only executes once. (#3100) * Change word usage: "fled" to "escaped" (#3118) "fled" is confusing to lot of people and is easily confused with pokemon vanishing. "escaped" is a better term. * Update the example config file (#3120) * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Rename path.example.json to path.json.example * typo: logrmation -> information (#2601) Fix a typo. I assume that it was "information" initially, but became "logrmation" when someone used replace all functionality to replace all infos with logs. But I might be totally wrong at this point, idk. Just didn't like the word and wanted to fix that typo. * Change fled to escaped (#3129) Fix an issue after PR #3118 * When JSON parsing fails, give a rough indication of why (#3137) * When JSON parsing fails, give a rough indication of why * Use the official package instead of SHA1 commit * Handle Github Download Zip Format (#3108) * Checking github plugin file existence * Loading plugins from github * Starting install code for github plugins * Updating GithubPlugin to support extracting folders * Handling github zip formats by extracting to the correct location --- .github/ISSUE_TEMPLATE.md | 2 + .gitignore | 5 + CONTRIBUTORS.md | 1 + Dockerfile | 4 +- configs/config.json.cluster.example | 1 + configs/config.json.example | 1 + configs/config.json.map.example | 1 + configs/config.json.path.example | 1 + configs/config.json.pokemon.example | 1 + .../{path.example.json => path.json.example} | 0 pokecli.py | 35 ++- pokemongo_bot/__init__.py | 65 ++++- .../cell_workers/move_to_map_pokemon.py | 227 +++++++++++++++--- .../cell_workers/pokemon_catch_worker.py | 4 +- .../cell_workers/update_title_stats.py | 2 + pokemongo_bot/health_record/bot_event.py | 2 +- pokemongo_bot/plugin_loader.py | 119 ++++++++- pokemongo_bot/plugins/.keep | 1 + pokemongo_bot/test/plugin_loader_test.py | 128 +++++++++- .../test/resources/plugin_fixture_test.zip | Bin 3412 -> 1939 bytes pokemongo_bot/test/resources/plugin_sha/.sha | 1 + ...54eddde33061be9b329efae0cfb9bd58842655.zip | Bin 0 -> 1734 bytes requirements.txt | 1 + tests/tree_config_builder_test.py | 4 +- 24 files changed, 546 insertions(+), 60 deletions(-) rename configs/{path.example.json => path.json.example} (100%) create mode 100644 pokemongo_bot/plugins/.keep create mode 100644 pokemongo_bot/test/resources/plugin_sha/.sha create mode 100644 pokemongo_bot/test/resources/test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 9976991cf9..a1d7168e75 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,5 @@ +Please check configuration at http://jsonlint.com/ before posting an issue. + ### Expected Behavior diff --git a/.gitignore b/.gitignore index a12509c322..70ab34b250 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,9 @@ var/ .pydevproject .settings/ +# Cloud9 Users +.c9/ + # Installer logs pip-log.txt pip-delete-this-directory.txt @@ -86,6 +89,8 @@ celerybeat-schedule # virtualenv venv/ ENV/ +local/ +share/ # Spyder project settings .spyderproject diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ee5aa7063d..6dceaf0918 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -53,3 +53,4 @@ * lucasfevi * Moonlight-Angel * mjmadsen + * nikofil diff --git a/Dockerfile b/Dockerfile index dce398c63e..f98d5d6942 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,9 @@ RUN cd /tmp && wget "http://pgoapi.com/pgoencrypt.tar.gz" \ && tar zxvf pgoencrypt.tar.gz \ && cd pgoencrypt/src \ && make \ - && cp libencrypt.so /usr/src/app/encrypt.so + && cp libencrypt.so /usr/src/app/encrypt.so \ + && cd /tmp \ + && rm -rf /tmp/pgoencrypt* VOLUME ["/usr/src/app/web"] diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index 8d0d8f854f..b32eb4f668 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.example b/configs/config.json.example index 20ef72e34e..ec46c15eb9 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.map.example b/configs/config.json.map.example index e665d4c6da..1079c999f9 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.path.example b/configs/config.json.path.example index afd1e3afeb..94a9fdba07 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 7cad1ac066..1d428a6ae7 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/path.example.json b/configs/path.json.example similarity index 100% rename from configs/path.example.json rename to configs/path.json.example diff --git a/pokecli.py b/pokecli.py index 24a0f38ee3..55afa55399 100644 --- a/pokecli.py +++ b/pokecli.py @@ -42,6 +42,12 @@ from pokemongo_bot.health_record import BotEvent from pokemongo_bot.plugin_loader import PluginLoader +try: + from demjson import jsonlint +except ImportError: + # Run `pip install -r requirements.txt` to fix this + jsonlint = None + if sys.version_info >= (2, 7, 9): ssl._create_default_https_context = ssl._create_unverified_context @@ -53,7 +59,7 @@ def main(): bot = False - + try: logger.info('PokemonGO Bot v1.0') sys.stdout = codecs.getwriter('utf8')(sys.stdout) @@ -104,7 +110,7 @@ def main(): 'api_error', sender=bot, level='info', - formmated='Log logged in, reconnecting in {:s}'.format(wait_time) + formatted='Log logged in, reconnecting in {:d}'.format(wait_time) ) time.sleep(wait_time) except ServerBusyOrOfflineException: @@ -130,7 +136,7 @@ def main(): if bot: report_summary(bot) - raise e + raise def report_summary(bot): if bot.metrics.start_time is None: @@ -162,16 +168,28 @@ def init_config(): # If config file exists, load variables from json load = {} + def _json_loader(filename): + try: + with open(filename, 'rb') as data: + load.update(json.load(data)) + except ValueError: + if jsonlint: + with open(filename, 'rb') as data: + lint = jsonlint() + rc = lint.main(['-v', filename]) + + logger.critical('Error with configuration file') + sys.exit(-1) + # Select a config file code parser.add_argument("-cf", "--config", help="Config File to use") config_arg = parser.parse_known_args() and parser.parse_known_args()[0].config or None + if config_arg and os.path.isfile(config_arg): - with open(config_arg) as data: - load.update(json.load(data)) + _json_loader(config_arg) elif os.path.isfile(config_file): logger.info('No config argument specified, checking for /configs/config.json') - with open(config_file) as data: - load.update(json.load(data)) + _json_loader(config_file) else: logger.info('Error: No /configs/config.json or specified config') @@ -384,6 +402,7 @@ def init_config(): if not config.password and 'password' not in load: config.password = getpass("Password: ") + config.encrypt_location = load.get('encrypt_location','') config.catch = load.get('catch', {}) config.release = load.get('release', {}) config.action_wait_max = load.get('action_wait_max', 4) @@ -446,7 +465,7 @@ def task_configuration_error(flag_name): plugin_loader = PluginLoader() for plugin in config.plugins: - plugin_loader.load_path(plugin) + plugin_loader.load_plugin(plugin) # create web dir if not exists try: diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index d0f034f110..8e9a28ce3a 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -9,6 +9,8 @@ import re import sys import time +import Queue +import threading from geopy.geocoders import GoogleV3 from pgoapi import PGoApi @@ -69,6 +71,11 @@ def __init__(self, config): # Make our own copy of the workers for this instance self.workers = [] + # Theading setup for file writing + self.web_update_queue = Queue.Queue(maxsize=1) + self.web_update_thread = threading.Thread(target=self.update_web_location_worker) + self.web_update_thread.start() + def start(self): self._setup_event_system() self._setup_logging() @@ -249,7 +256,7 @@ def _register_events(self): ) ) self.event_manager.register_event( - 'pokemon_fled', + 'pokemon_escaped', parameters=('pokemon',) ) self.event_manager.register_event( @@ -393,6 +400,35 @@ def _register_events(self): ) self.event_manager.register_event('unset_pokemon_nickname') + # Move To map pokemon + self.event_manager.register_event( + 'move_to_map_pokemon_fail', + parameters=('message',) + ) + self.event_manager.register_event( + 'move_to_map_pokemon_updated_map', + parameters=('lat', 'lon') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_teleport_to', + parameters=('poke_name', 'poke_dist', 'poke_lat', 'poke_lon', + 'disappears_in') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_encounter', + parameters=('poke_name', 'poke_dist', 'poke_lat', 'poke_lon', + 'disappears_in') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_move_towards', + parameters=('poke_name', 'poke_dist', 'poke_lat', 'poke_lon', + 'disappears_in') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_teleport_back', + parameters=('last_lat', 'last_lon') + ) + def tick(self): self.health_record.heartbeat() self.cell = self.get_meta_cell() @@ -607,7 +643,6 @@ def login(self): ) def get_encryption_lib(self): - file_name = '' if _platform == "linux" or _platform == "linux2" or _platform == "darwin": file_name = 'encrypt.so' elif _platform == "Windows" or _platform == "win32": @@ -617,15 +652,18 @@ def get_encryption_lib(self): else: file_name = 'encrypt.dll' - path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) - full_path = path + '/'+ file_name + if self.config.encrypt_location == '': + path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) + else: + path = self.config.encrypt_location + full_path = path + '/'+ file_name if not os.path.isfile(full_path): - self.logger.error(file_name + ' is not found! Please place it in the bots root directory.') - self.logger.info('Platform: '+ _platform + ' Bot root directory: '+ path) + self.logger.error(file_name + ' is not found! Please place it in the bots root directory or set libencrypt_location in config.') + self.logger.info('Platform: '+ _platform + ' Encrypt.so directory: '+ path) sys.exit(1) else: - self.logger.info('Found '+ file_name +'! Platform: ' + _platform + ' Bot root directory: ' + path) + self.logger.info('Found '+ file_name +'! Platform: ' + _platform + ' Encrypt.so directory: ' + path) return full_path @@ -716,7 +754,8 @@ def _print_character_info(self): self.logger.info( 'Potion: ' + str(items_stock[101]) + ' | SuperPotion: ' + str(items_stock[102]) + - ' | HyperPotion: ' + str(items_stock[103])) + ' | HyperPotion: ' + str(items_stock[103]) + + ' | MaxPotion: ' + str(items_stock[104])) self.logger.info( 'Incense: ' + str(items_stock[401]) + @@ -944,7 +983,15 @@ def heartbeat(self): request.get_player() request.check_awarded_badges() request.call() - self.update_web_location() # updates every tick + try: + self.web_update_queue.put_nowait(True) # do this outside of thread every tick + except Queue.Full: + pass + + def update_web_location_worker(self): + while True: + self.web_update_queue.get() + self.update_web_location() def get_inventory_count(self, what): response_dict = self.get_inventory() diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 08ff35f281..9ab6bbb7a2 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -1,11 +1,59 @@ # -*- coding: utf-8 -*- +""" +Moves a trainer to a Pokemon. + +Events: + move_to_map_pokemon_fail + When the worker fails. + Returns: + message: Failure message. + + move_to_map_pokemon_updated_map + When worker updates the PokemonGo-Map. + Returns: + lat: Latitude + lon: Longitude + + move_to_map_pokemon_teleport_to + When trainer is teleported to a Pokemon. + Returns: + poke_name: Pokemon's name + poke_dist: Distance from the trainer + poke_lat: Latitude of the Pokemon + poke_lon: Longitude of the Pokemon + disappears_in: Number of seconds before the Pokemon disappears + + move_to_map_pokemon_encounter + When a trainer encounters a Pokemon by teleporting or walking. + Returns: + poke_name: Pokemon's name + poke_dist: Distance from the trainer + poke_lat: Latitude of the Pokemon + poke_lon: Longitude of the Pokemon + disappears_in: Number of seconds before the Pokemon disappears + + move_to_map_pokemon_move_towards + When a trainer moves toward a Pokemon. + Returns: + poke_name: Pokemon's name + poke_dist: Distance from the trainer + poke_lat: Latitude of the Pokemon + poke_lon: Longitude of the Pokemon + disappears_in: Number of seconds before the Pokemon disappears + + move_to_map_pokemon_teleport_back + When a trainer teleports back to thier previous location. + Returns: + last_lat: Trainer's last known latitude + last_lon: Trainer's last known longitude + +""" import os import time import json import base64 import requests -from pokemongo_bot import logger from pokemongo_bot.cell_workers.utils import distance, format_dist, format_time from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult @@ -13,7 +61,20 @@ from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker +# Update the map if more than N meters away from the center. (AND'd with +# UPDATE_MAP_MIN_TIME_MINUTES) +UPDATE_MAP_MIN_DISTANCE_METERS = 500 + +# Update the map if it hasn't been updated in n seconds. (AND'd with +# UPDATE_MAP_MIN_DISTANCE_METERS) +UPDATE_MAP_MIN_TIME_SEC = 120 + +# Number of seconds to sleep between teleporting to a snipped Pokemon. +SNIPE_SLEEP_SEC = 2 + + class MoveToMapPokemon(BaseTask): + """Task for moving a trainer to a Pokemon.""" SUPPORTED_TASK_API_VERSION = 1 def initialize(self): @@ -32,13 +93,15 @@ def get_pokemon_from_map(self): try: req = requests.get('{}/raw_data?gyms=false&scanned=false'.format(self.config['address'])) except requests.exceptions.ConnectionError: - logger.log('Could not reach PokemonGo-Map Server', 'red') + self._emit_failure('Could not get Pokemon data from PokemonGo-Map: ' + '{}. Is it running?'.format( + self.config['address'])) return [] try: raw_data = req.json() except ValueError: - logger.log('Map data was not valid', 'red') + self._emit_failure('Map data was not valid') return [] pokemon_list = [] @@ -48,7 +111,7 @@ def get_pokemon_from_map(self): try: pokemon['encounter_id'] = long(base64.b64decode(pokemon['encounter_id'])) except TypeError: - log.logger('base64 error: {}'.format(pokemon['encounter_id']), 'red') + self._emit_failure('base64 error: {}'.format(pokemon['encounter_id'])) continue pokemon['spawn_point_id'] = pokemon['spawnpoint_id'] pokemon['disappear_time'] = int(pokemon['disappear_time'] / 1000) @@ -100,14 +163,17 @@ def update_map_location(self): try: req = requests.get('{}/loc'.format(self.config['address'])) except requests.exceptions.ConnectionError: - logger.log('Could not reach PokemonGo-Map Server', 'red') + self._emit_failure('Could not update trainer location ' + 'PokemonGo-Map: {}. Is it running?'.format( + self.config['address'])) return try: loc_json = req.json() except ValueError: - return log.logger('Map location data was not valid', 'red') - + err = 'Map location data was not valid' + self._emit_failure(err) + return log.logger(err, 'red') dist = distance( self.bot.position[0], @@ -118,32 +184,40 @@ def update_map_location(self): # update map when 500m away from center and last update longer than 2 minutes away now = int(time.time()) - if dist > 500 and now - self.last_map_update > 2 * 60: - requests.post('{}/next_loc?lat={}&lon={}'.format(self.config['address'], self.bot.position[0], self.bot.position[1])) - logger.log('Updated PokemonGo-Map position') + if (dist > UPDATE_MAP_MIN_DISTANCE_METERS and + now - self.last_map_update > UPDATE_MAP_MIN_TIME_SEC): + requests.post( + '{}/next_loc?lat={}&lon={}'.format(self.config['address'], + self.bot.position[0], + self.bot.position[1])) + self.emit_event( + 'move_to_map_pokemon_updated_map', + formatted='Updated PokemonGo-Map to {lat}, {lon}', + data={ + 'lat': self.bot.position[0], + 'lon': self.bot.position[1] + } + ) self.last_map_update = now def snipe(self, pokemon): - last_position = self.bot.position[0:2] + """Snipe a Pokemon by teleporting. + Args: + pokemon: Pokemon to snipe. + """ + last_position = self.bot.position[0:2] self.bot.heartbeat() - - logger.log('Teleporting to {} ({})'.format(pokemon['name'], format_dist(pokemon['dist'], self.unit)), 'green') - self.bot.api.set_position(pokemon['latitude'], pokemon['longitude'], 0) - - logger.log('Encounter pokemon', 'green') + self._teleport_to(pokemon) catch_worker = PokemonCatchWorker(pokemon, self.bot) api_encounter_response = catch_worker.create_encounter_api_call() - - time.sleep(2) - logger.log('Teleporting back to previous location..', 'green') + time.sleep(SNIPE_SLEEP_SEC) + self._teleport_back(last_position) self.bot.api.set_position(last_position[0], last_position[1], 0) - time.sleep(2) + time.sleep(SNIPE_SLEEP_SEC) self.bot.heartbeat() - catch_worker.work(api_encounter_response) self.add_caught(pokemon) - return WorkerResult.SUCCESS def dump_caught_pokemon(self): @@ -182,18 +256,109 @@ def work(self): if self.config['snipe']: return self.snipe(pokemon) + step_walker = self._move_to(pokemon) + if not step_walker.step(): + return WorkerResult.RUNNING + self._encountered(pokemon) + self.add_caught(pokemon) + return WorkerResult.SUCCESS + + def _emit_failure(self, msg): + """Emits failure to event log. + + Args: + msg: Message to emit + """ + self.emit_event( + 'move_to_map_pokemon_fail', + formatted='Failure! {message}', + data={'message': msg} + ) + + def _emit_log(self, msg): + """Emits log to event log. + + Args: + msg: Message to emit + """ + self.emit_event( + 'move_to_map_pokemon', + formatted='{message}', + data={'message': msg} + ) + + def _pokemon_event_data(self, pokemon): + """Generates parameters used for the Bot's event manager. + + Args: + pokemon: Pokemon object + + Returns: + Dictionary with Pokemon's info. + """ now = int(time.time()) - logger.log('Moving towards {}, {} left ({})'.format(pokemon['name'], format_dist(pokemon['dist'], self.unit), format_time(pokemon['disappear_time'] - now))) - step_walker = StepWalker( + return { + 'poke_name': pokemon['name'], + 'poke_dist': (format_dist(pokemon['dist'], self.unit)), + 'poke_lat': pokemon['latitude'], + 'poke_lon': pokemon['longitude'], + 'disappears_in': (format_time(pokemon['disappear_time'] - now)) + } + + def _teleport_to(self, pokemon): + """Teleports trainer to a Pokemon. + + Args: + pokemon: Pokemon to teleport to. + """ + self.emit_event( + 'move_to_map_pokemon_teleport_to', + formatted='Teleporting to {poke_name}. ({poke_dist})', + data=self._pokemon_event_data(pokemon) + ) + self.bot.api.set_position(pokemon['latitude'], pokemon['longitude'], 0) + self._encountered(pokemon) + + def _encountered(self, pokemon): + """Emit event when trainer encounters a Pokemon. + + Args: + pokemon: Pokemon encountered. + """ + self.emit_event( + 'move_to_map_pokemon_encounter', + formatted='Encountered Pokemon: {poke_name}', + data=self._pokemon_event_data(pokemon) + ) + + def _teleport_back(self, last_position): + """Teleports trainer back to their last position.""" + self.emit_event( + 'move_to_map_pokemon_teleport_back', + formatted=('Teleporting back to previous location ({last_lat}, ' + '{last_lon})'), + data={'last_lat': last_position[0], 'last_lon': last_position[1]} + ) + + def _move_to(self, pokemon): + """Moves trainer towards a Pokemon. + + Args: + pokemon: Pokemon to move to. + + Returns: + StepWalker + """ + now = int(time.time()) + self.emit_event( + 'move_to_map_pokemon_move_towards', + formatted=('Moving towards {poke_name}, {poke_dist}, left (' + '{disappears_in})'), + data=self._pokemon_event_data(pokemon) + ) + return StepWalker( self.bot, self.bot.config.walk, pokemon['latitude'], pokemon['longitude'] ) - - if not step_walker.step(): - return WorkerResult.RUNNING - - logger.log('Arrived at {}'.format(pokemon['name'])) - self.add_caught(pokemon) - return WorkerResult.SUCCESS diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index d676e9f0e2..d5118b391b 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -324,8 +324,8 @@ def work(self, response_dict=None): 'CATCH_POKEMON']['status'] if status is 2: self.emit_event( - 'pokemon_fled', - formatted="{pokemon} fled.", + 'pokemon_escaped', + formatted="{pokemon} escaped.", data={'pokemon': pokemon_name} ) sleep(2) diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index 43e55c260f..acbfaa7fe4 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -111,8 +111,10 @@ def _update_title(self, title, platform): """ if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) + stdout.flush() elif platform == "darwin": stdout.write("\033]0;{}\007".format(title)) + stdout.flush() elif platform == "win32": ctypes.windll.kernel32.SetConsoleTitleA(title) else: diff --git a/pokemongo_bot/health_record/bot_event.py b/pokemongo_bot/health_record/bot_event.py index f357a4e8e7..55a726049d 100644 --- a/pokemongo_bot/health_record/bot_event.py +++ b/pokemongo_bot/health_record/bot_event.py @@ -16,7 +16,7 @@ def __init__(self, config): # UniversalAnalytics can be reviewed here: # https://github.com/analytics-pros/universal-analytics-python if self.config.health_record: - self.logger.info('Health check is enabled. For more logrmation:') + self.logger.info('Health check is enabled. For more information:') self.logger.info('https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev#analytics') self.client = Client( dsn='https://8abac56480f34b998813d831de262514:196ae1d8dced41099f8253ea2c8fe8e6@app.getsentry.com/90254', diff --git a/pokemongo_bot/plugin_loader.py b/pokemongo_bot/plugin_loader.py index 3bded030b3..f7e12a85a7 100644 --- a/pokemongo_bot/plugin_loader.py +++ b/pokemongo_bot/plugin_loader.py @@ -1,6 +1,10 @@ import os import sys import importlib +import re +import requests +import zipfile +import shutil class PluginLoader(object): folder_cache = [] @@ -15,8 +19,16 @@ def _get_correct_path(self, path): return correct_path - def load_path(self, path): - correct_path = self._get_correct_path(path) + def load_plugin(self, plugin): + github_plugin = GithubPlugin(plugin) + if github_plugin.is_valid_plugin(): + if not github_plugin.is_already_installed(): + github_plugin.install() + + correct_path = github_plugin.get_plugin_folder() + + else: + correct_path = self._get_correct_path(plugin) if correct_path not in self.folder_cache: self.folder_cache.append(correct_path) @@ -25,9 +37,112 @@ def load_path(self, path): def remove_path(self, path): correct_path = self._get_correct_path(path) sys.path.remove(correct_path) + self.folder_cache.remove(correct_path) def get_class(self, namespace_class): [namespace, class_name] = namespace_class.split('.') my_module = importlib.import_module(namespace) return getattr(my_module, class_name) +class GithubPlugin(object): + PLUGINS_FOLDER = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'plugins') + + def __init__(self, plugin_name): + self.plugin_name = plugin_name + self.plugin_parts = self.get_github_parts() + + def is_valid_plugin(self): + return self.plugin_parts is not None + + def get_github_parts(self): + groups = re.match('(.*)\/(.*)#(.*)', self.plugin_name) + + if groups is None: + return None + + parts = {} + parts['user'] = groups.group(1) + parts['repo'] = groups.group(2) + parts['sha'] = groups.group(3) + + return parts + + def get_installed_version(self): + if not self.is_already_installed(): + return None + + filename = os.path.join(self.get_plugin_folder(), '.sha') + print filename + with open(filename) as file: + return file.read().strip() + + def get_local_destination(self): + parts = self.plugin_parts + if parts is None: + raise Exception('Not a valid github plugin') + + file_name = '{}_{}_{}.zip'.format(parts['user'], parts['repo'], parts['sha']) + full_path = os.path.join(self.PLUGINS_FOLDER, file_name) + return full_path + + def is_already_installed(self): + file_path = self.get_plugin_folder() + if not os.path.isdir(file_path): + return False + + sha_file = os.path.join(file_path, '.sha') + + if not os.path.isfile(sha_file): + return False + + with open(sha_file) as file: + content = file.read().strip() + + if content != self.plugin_parts['sha']: + return False + + return True + + def get_plugin_folder(self): + folder_name = '{}_{}'.format(self.plugin_parts['user'], self.plugin_parts['repo']) + return os.path.join(self.PLUGINS_FOLDER, folder_name) + + def get_github_download_url(self): + parts = self.plugin_parts + if parts is None: + raise Exception('Not a valid github plugin') + + github_url = 'https://github.com/{}/{}/archive/{}.zip'.format(parts['user'], parts['repo'], parts['sha']) + return github_url + + def install(self): + self.download() + self.extract() + + def extract(self): + dest = self.get_plugin_folder() + with zipfile.ZipFile(self.get_local_destination(), "r") as z: + z.extractall(dest) + + github_folder = os.path.join(dest, '{}-{}'.format(self.plugin_parts['repo'], self.plugin_parts['sha'])) + new_folder = os.path.join(dest, '{}'.format(self.plugin_parts['repo'])) + shutil.move(github_folder, new_folder) + + with open(os.path.join(dest, '.sha'), 'w') as file: + file.write(self.plugin_parts['sha']) + + os.remove(self.get_local_destination()) + + def download(self): + url = self.get_github_download_url() + dest = self.get_local_destination() + + r = requests.get(url, stream=True) + r.raise_for_status() + + with open(dest, 'wb') as f: + for chunk in r.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) + r.close() + return dest diff --git a/pokemongo_bot/plugins/.keep b/pokemongo_bot/plugins/.keep new file mode 100644 index 0000000000..5d848b8301 --- /dev/null +++ b/pokemongo_bot/plugins/.keep @@ -0,0 +1 @@ +keep this so we can install plugins into this folder diff --git a/pokemongo_bot/test/plugin_loader_test.py b/pokemongo_bot/test/plugin_loader_test.py index 4d4d5ca952..ed285ede67 100644 --- a/pokemongo_bot/test/plugin_loader_test.py +++ b/pokemongo_bot/test/plugin_loader_test.py @@ -4,25 +4,145 @@ import importlib import unittest import os +import shutil +import mock from datetime import timedelta, datetime from mock import patch, MagicMock -from pokemongo_bot.plugin_loader import PluginLoader +from pokemongo_bot.plugin_loader import PluginLoader, GithubPlugin from pokemongo_bot.test.resources.plugin_fixture import FakeTask +PLUGIN_PATH = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins')) + class PluginLoaderTest(unittest.TestCase): def setUp(self): self.plugin_loader = PluginLoader() def test_load_namespace_class(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') - self.plugin_loader.load_path(package_path) + self.plugin_loader.load_plugin(package_path) loaded_class = self.plugin_loader.get_class('plugin_fixture.FakeTask') self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') self.plugin_loader.remove_path(package_path) def test_load_zip(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture_test.zip') - self.plugin_loader.load_path(package_path) + self.plugin_loader.load_plugin(package_path) loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') - self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTaskZip') self.plugin_loader.remove_path(package_path) + + def copy_plugin(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') + dest_path = os.path.join(PLUGIN_PATH, 'org_repo', 'plugin_fixture_tests') + shutil.copytree(package_path, os.path.join(dest_path)) + with open(os.path.join(os.path.dirname(dest_path), '.sha'), 'w') as file: + file.write('testsha') + return dest_path + + def test_load_github_already_downloaded(self): + dest_path = self.copy_plugin() + self.plugin_loader.load_plugin('org/repo#testsha') + loaded_class = self.plugin_loader.get_class('plugin_fixture_tests.FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + self.plugin_loader.remove_path(dest_path) + shutil.rmtree(os.path.dirname(dest_path)) + + def copy_zip(self): + zip_name = 'test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip' + fixture_zip = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', zip_name) + zip_dest = os.path.join(PLUGIN_PATH, 'org_test-pgo-plugin_2d54eddde33061be9b329efae0cfb9bd58842655.zip') + shutil.copyfile(fixture_zip, zip_dest) + + @mock.patch.object(GithubPlugin, 'download', copy_zip) + def test_load_github_not_downloaded(self): + self.plugin_loader.load_plugin('org/test-pgo-plugin#2d54eddde33061be9b329efae0cfb9bd58842655') + loaded_class = self.plugin_loader.get_class('test-pgo-plugin.PrintText') + self.assertEqual(loaded_class({}, {}).work(), 'PrintText') + dest_path = os.path.join(PLUGIN_PATH, 'org_test-pgo-plugin') + self.plugin_loader.remove_path(os.path.join(dest_path, 'test-pgo-plugin')) + shutil.rmtree(dest_path) + +class GithubPluginTest(unittest.TestCase): + def test_get_github_parts_for_valid_github(self): + github_plugin = GithubPlugin('org/repo#sha') + self.assertTrue(github_plugin.is_valid_plugin()) + self.assertEqual(github_plugin.plugin_parts['user'], 'org') + self.assertEqual(github_plugin.plugin_parts['repo'], 'repo') + self.assertEqual(github_plugin.plugin_parts['sha'], 'sha') + + def test_get_github_parts_for_invalid_github(self): + self.assertFalse(GithubPlugin('org/repo').is_valid_plugin()) + self.assertFalse(GithubPlugin('foo').is_valid_plugin()) + self.assertFalse(GithubPlugin('/Users/foo/bar.zip').is_valid_plugin()) + + def test_get_installed_version(self): + github_plugin = GithubPlugin('org/repo#my-version') + src_fixture = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_sha') + dest = github_plugin.get_plugin_folder() + shutil.copytree(src_fixture, dest) + actual = github_plugin.get_installed_version() + shutil.rmtree(dest) + self.assertEqual('my-version', actual) + + def test_get_plugin_folder(self): + github_plugin = GithubPlugin('org/repo#sha') + expected = os.path.join(PLUGIN_PATH, 'org_repo') + actual = github_plugin.get_plugin_folder() + self.assertEqual(actual, expected) + + def test_get_local_destination(self): + github_plugin = GithubPlugin('org/repo#sha') + path = github_plugin.get_local_destination() + expected = os.path.join(PLUGIN_PATH, 'org_repo_sha.zip') + self.assertEqual(path, expected) + + def test_get_github_download_url(self): + github_plugin = GithubPlugin('org/repo#sha') + url = github_plugin.get_github_download_url() + expected = 'https://github.com/org/repo/archive/sha.zip' + self.assertEqual(url, expected) + + def test_is_already_installed_not_installed(self): + github_plugin = GithubPlugin('org/repo#sha') + self.assertFalse(github_plugin.is_already_installed()) + + def test_is_already_installed_version_mismatch(self): + github_plugin = GithubPlugin('org/repo#sha') + plugin_folder = github_plugin.get_plugin_folder() + os.mkdir(plugin_folder) + with open(os.path.join(plugin_folder, '.sha'), 'w') as file: + file.write('sha2') + + actual = github_plugin.is_already_installed() + shutil.rmtree(plugin_folder) + self.assertFalse(actual) + + def test_is_already_installed_installed(self): + github_plugin = GithubPlugin('org/repo#sha') + plugin_folder = github_plugin.get_plugin_folder() + os.mkdir(plugin_folder) + with open(os.path.join(plugin_folder, '.sha'), 'w') as file: + file.write('sha') + + actual = github_plugin.is_already_installed() + shutil.rmtree(plugin_folder) + self.assertTrue(actual) + + def test_extract(self): + github_plugin = GithubPlugin('org/test-pgo-plugin#2d54eddde33061be9b329efae0cfb9bd58842655') + src = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip') + zip_dest = github_plugin.get_local_destination() + shutil.copyfile(src, zip_dest) + github_plugin.extract() + plugin_folder = github_plugin.get_plugin_folder() + os.path.isdir(plugin_folder) + sub_folder = os.path.join(plugin_folder, 'test-pgo-plugin') + os.path.isdir(sub_folder) + sha_file = os.path.join(github_plugin.get_plugin_folder(), '.sha') + os.path.isfile(sha_file) + + with open(sha_file) as file: + content = file.read().strip() + self.assertEqual(content, '2d54eddde33061be9b329efae0cfb9bd58842655') + + shutil.rmtree(plugin_folder) diff --git a/pokemongo_bot/test/resources/plugin_fixture_test.zip b/pokemongo_bot/test/resources/plugin_fixture_test.zip index 335d95e5226056902bb02114fa26a2b3dad61ca1..78828798c32b46cb56eac6df953e7add47c175cf 100644 GIT binary patch delta 564 zcmca2HJP6$z?+$civa`#w@>8Jl2Daj5iSJ8Ul~OhKyo4s96%_teWIT+y8uvJTzBe z?UTP2rKskudemEWZEnUbYd?z(IW;{;`c(fVsP44FwpL~y9Tq!hyg8^X%D;p?TfTDjV69dB$77z~reUPTy delta 1852 zcmbQte?^KXz?+$civa|hB`5M|Ni>Nq4=?3h9{!b4gaITc!oUH9*^(3ejM+PYa-N*a zCw5$9`lT>Ano(W?Bm%-P)ew__l9L#XF(hF|O}@)$2QzE35>pp*wb=5>>zSgNnk6Um zFsn_T%fwxu92&vH02GJ2C3J$f7mJ}loA%u%egPMortW(gdJ2p5N|tt5b!_I$Ek7$@ zeOEL{^n!4u1OI}lnT|R^@;_Jqvw6M#@7~*Q&p+p@TYPEHg(u0b>km6HNbu_L&RC?) zA=+cAlYQ0H#w!PKT7Ud{i%gXhJ}3-CneNN*1o)(D|TvK($X^Z zsA>)K3CgbjKJPaBwKKXb`+@uq*F^E2PF=SA1-Y9&tvsxub#AZ$Ld+zUtRsO=&js}f~1>2B)a8U!`+N;<}#bJOulBY_=Y|AH+GN< zjqOD|Yk@AD1jLBM(JaaC2^4?>MFxS%A6Y~vvGZU?Ha(dCfU_y# zJCoA3DAyxqDr=MKrkUKFGGmGKrsymAd%M}cFj#HQ?2cRDU3TL3+{*mty}xU3RNwet z<(05E=FapTUp3oA?AcTujw&P_2x3=h?s0r|Nc{l&f#N3)w;C_pKK$hmd;Rp|AI{4p zNF0muOOA~w5Ed89Y1PQ}a5^rbB4B??P*(ZfiB^R#GD%C%w5;{ch!2cpfAjd(oI;1^ zav!(-Dp{R6bE~^ZNlWATEjcFBQ_sD2+k4_j#=F%{VOf={&$XTUu*;QO^;t*Hl{WLB z)uEF;&b}_%dSbfL>HB9^O|&^YujSyf`d?pnht`DpUR2dRUiPBYER17gkG5vZ%gBJ0 zC80t#juPi;>VXr zkcQ=D^yt!oMAu{i7IDn@IvR8v9$yOkIO-e)-u!FmSSu#Dk=Jwejp?en+f>a?>fPjh zwfSGS{0sIMXSRiQ*}a}*`=rIP_?giE+U=#A|CX7zUj6uS-a}J&4oy3rqK2k}GZ?*X zC-9{41j)xS#xcA*F1LiYqgj{de7)TV7M}ZW_?*lQi)PGdz3XnI<-2z4sx>~=R*TQa zPg8j@=_z9qOOaEu*O?QkZ=6Ng<({hFm@s$p8pd^JQhn~TvVIWeS;l5(w8<#undJ4U zRTF}Ydi_P#q@3e)-CQ>TgPkuMlHMQZ^MeEQ*)o>`xm_S6ASG0`%&M$pmM&Bdd;b8 zr*C|=V+6&S>c+C#5MZ2r1IHO7lPEJ60~;t00ZYM4EEZz0f;hk%BnK}$6<8g`pyhBl zL<(N!mb2=Dr9jeXQoEqK5K?dh1=)Pyg>ed7kP@uKLADo^WDup(Z@U diff --git a/pokemongo_bot/test/resources/plugin_sha/.sha b/pokemongo_bot/test/resources/plugin_sha/.sha new file mode 100644 index 0000000000..eaf604c9ac --- /dev/null +++ b/pokemongo_bot/test/resources/plugin_sha/.sha @@ -0,0 +1 @@ +my-version diff --git a/pokemongo_bot/test/resources/test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip b/pokemongo_bot/test/resources/test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip new file mode 100644 index 0000000000000000000000000000000000000000..a692ac3f08492882a28820569a62b59d2647006f GIT binary patch literal 1734 zcmWIWW@h1H00EUv?4Do-l(1yrWGG23F3~MW&(|%;DNWDJ(=|#lHAziLNl7&}HZU_x zO0`TfHnL1jOH4ILPD`>(N-?#tFflSSHPsIdVP#;vD!L*(0BSx5*!)Ruz2?453=EZY+*mHVk{C#!H>m@IgCcmF3;%OWIa(3v7jeA_4XI_~5Oe3#w|LWT6`t|M)BHw;5 z+n=YH5|>_-JvUN%$C}s*HSH>gtm6wN z>+z<~X`LdrR!?om#7$FL-8LR)Hxx0cST=G(^n!Ao*PN!P%_x4c&Yp4}6yT<(=;iumAbz&AQdA zZ&miDu6pyqY{&0vBgJIj^}ix^Xmn;rZ$Gc*vu3l*@sG+0XBOK;KmNYMRbe{M*{KKg z1#UkS3ZHKp;j~)+igT;V&YQQKUzpGKiP7JfyDm@l(VIh&{5k#HtSfmP)6B|$hs%cL z-Vu>b?(h`$WPNXT_7%I>tC{=(VKL78kN18PkUSq&-?GE$%Bs6dE+vRQovzVx^zsr> zAwwqCvnDcs)l9Y;wr1$LcuEumYKHRdP@m2srF(DXKlAzWbtShww4<&X7K&a;&784I zXnjTJpHmb62~TH;U-XXwHQCl5-?K;#m@VXi*oBg08y}yUmst`YuUAkBPq*IMr_P6- zSlgqoaq5hA;E6N3C-k)fIT#9^g0#JMpgF|$*O_^9fsQy&sY7xr;|q#1^Gc8%;@jiT z-Q*y0&rU{@JX(=-uSI>R&%z*2v5=-aj+ac?0i)BdZKc zIh0>MFi`i2ev^10>}y8W6xK`YPRyM&Ww+R@ItTB9c}#|t$4=>$^X Date: Mon, 8 Aug 2016 14:18:17 -0700 Subject: [PATCH 085/202] Refactor catch worker (#2527) * refactor catch worker * fix * few renames * add to contributors * fix * add missing behavior * fix encounter events * don't make events about ignored pokemon --- CONTRIBUTORS.md | 1 + pokemongo_bot/__init__.py | 9 +- .../cell_workers/pokemon_catch_worker.py | 799 ++++++++---------- 3 files changed, 357 insertions(+), 452 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 6dceaf0918..02aaacab4f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -51,6 +51,7 @@ * matheussampaio * Abraxas000 * lucasfevi + * pokepal * Moonlight-Angel * mjmadsen * nikofil diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 8e9a28ce3a..4ea6ac3b15 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -232,10 +232,12 @@ def _register_events(self): 'iv_display', ) ) + self.event_manager.register_event('no_pokeballs') self.event_manager.register_event( 'pokemon_catch_rate', parameters=( 'catch_rate', + 'ball_name', 'berry_name', 'berry_count' ) @@ -244,25 +246,28 @@ def _register_events(self): 'threw_berry', parameters=( 'berry_name', + 'ball_name', 'new_catch_rate' ) ) self.event_manager.register_event( 'threw_pokeball', parameters=( - 'pokeball', + 'ball_name', 'success_percentage', 'count_left' ) ) self.event_manager.register_event( - 'pokemon_escaped', + 'pokemon_capture_failed', parameters=('pokemon',) ) self.event_manager.register_event( 'pokemon_vanished', parameters=('pokemon',) ) + self.event_manager.register_event('pokemon_not_in_range') + self.event_manager.register_event('pokemon_inventory_full') self.event_manager.register_event( 'pokemon_caught', parameters=( diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index d5118b391b..a0711b49d6 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -1,13 +1,49 @@ # -*- coding: utf-8 -*- import time -from pokemongo_bot.human_behaviour import (normalized_reticle_size, sleep, - spin_modifier) from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.human_behaviour import normalized_reticle_size, sleep, spin_modifier + + +CATCH_STATUS_SUCCESS = 1 +CATCH_STATUS_FAILED = 2 +CATCH_STATUS_VANISHED = 3 + +ENCOUNTER_STATUS_SUCCESS = 1 +ENCOUNTER_STATUS_NOT_IN_RANGE = 5 +ENCOUNTER_STATUS_POKEMON_INVENTORY_FULL = 7 + +ITEM_POKEBALL = 1 +ITEM_GREATBALL = 2 +ITEM_ULTRABALL = 3 +ITEM_RAZZBERRY = 701 + +LOGIC_TO_FUNCTION = { + 'or': lambda x, y: x or y, + 'and': lambda x, y: x and y +} + + +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.cp = pokemon_data['cp'] + self.attack = pokemon_data.get('individual_attack', 0) + self.defense = pokemon_data.get('individual_defense', 0) + self.stamina = pokemon_data.get('individual_stamina', 0) + + @property + def iv(self): + return round((self.attack + self.defense + self.stamina) / 45.0, 2) + + @property + def iv_display(self): + return '{}/{}/{}'.format(self.attack, self.defense, self.stamina) + class PokemonCatchWorker(BaseTask): - BAG_FULL = 'bag_full' - NO_POKEBALLS = 'no_pokeballs' def __init__(self, pokemon, bot): self.pokemon = pokemon @@ -22,444 +58,63 @@ def __init__(self, pokemon, bot): self.response_key = '' self.response_status_key = '' + ############################################################################ + # public methods + ############################################################################ + def work(self, response_dict=None): - encounter_id = self.pokemon['encounter_id'] + response_dict = response_dict or self.create_encounter_api_call() + # validate response if not response_dict: - response_dict = self.create_encounter_api_call() - - if response_dict and 'responses' in response_dict: - if self.response_key in response_dict['responses']: - if self.response_status_key in response_dict['responses'][self.response_key]: - if response_dict['responses'][self.response_key][self.response_status_key] is 1: - cp = 0 - if 'wild_pokemon' in response_dict['responses'][self.response_key] or 'pokemon_data' in \ - response_dict['responses'][self.response_key]: - if self.response_key == 'ENCOUNTER': - pokemon = response_dict['responses'][self.response_key]['wild_pokemon'] - else: - pokemon = response_dict['responses'][self.response_key] - - catch_rate = response_dict['responses'][self.response_key]['capture_probability'][ - 'capture_probability'] # 0 = pokeballs, 1 great balls, 3 ultra balls - - if 'pokemon_data' in pokemon and 'cp' in pokemon['pokemon_data']: - pokemon_data = pokemon['pokemon_data'] - cp = pokemon_data['cp'] - - individual_attack = pokemon_data.get("individual_attack", 0) - individual_stamina = pokemon_data.get("individual_stamina", 0) - individual_defense = pokemon_data.get("individual_defense", 0) - - iv_display = '{}/{}/{}'.format( - individual_attack, - individual_defense, - individual_stamina - ) - - pokemon_potential = self.pokemon_potential(pokemon_data) - pokemon_num = int(pokemon_data['pokemon_id']) - 1 - pokemon_name = self.pokemon_list[int(pokemon_num)]['Name'] - - msg = 'A wild {pokemon} appeared! [CP {cp}] [Potential {iv}] [S/A/D {iv_display}]' - self.emit_event( - 'pokemon_appeared', - formatted=msg, - data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': pokemon_potential, - 'iv_display': iv_display, - } - ) - - pokemon_data['name'] = pokemon_name - # Simulate app - sleep(3) - - if not self.should_capture_pokemon(pokemon_name, cp, pokemon_potential, response_dict): - return False - - flag_VIP = False - # @TODO, use the best ball in stock to catch VIP (Very Important Pokemon: Configurable) - if self.check_vip_pokemon(pokemon_name, cp, pokemon_potential): - self.emit_event( - 'vip_pokemon', - formatted='This is a VIP pokemon. Catch!!!' - ) - flag_VIP=True - - items_stock = self.bot.current_inventory() - berry_id = 701 # @ TODO: use better berries if possible - berries_count = self.bot.item_inventory_count(berry_id) - while True: - # pick the most simple ball from stock - pokeball = 1 # start from 1 - PokeBalls - berry_used = False - - if flag_VIP: - if(berries_count>0 and catch_rate[pokeball-1] < 0.9): - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - self.emit_event( - 'pokemon_catch_rate', - level='debug', - formatted="Catch rate of {catch_rate} is low. Maybe will throw {berry_name} ({berry_count} left)", - data={ - 'catch_rate': success_percentage, - 'berry_name': self.item_list[str(berry_id)], - 'berry_count': berries_count - } - ) - # Out of all pokeballs! Let's don't waste berry. - if items_stock[1] == 0 and items_stock[2] == 0 and items_stock[3] == 0: - break - - # Use the berry to catch - 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']: - catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - berries_count = berries_count -1 - berry_used = True - self.emit_event( - 'threw_berry', - formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", - data={ - "berry_name": self.item_list[str(berry_id)], - "new_catch_rate": success_percentage - } - ) - else: - if response_dict['status_code'] is 1: - self.emit_event( - 'softban', - level='warning', - formatted='Failed to use berry. You may be softbanned.' - ) - else: - self.emit_event( - 'threw_berry_failed', - formatted='Unknown response when throwing berry: {status_code}.', - data={ - 'status_code': response_dict['status_code'] - } - ) - - #use the best ball to catch - current_type = pokeball - #debug use normal ball - while current_type < 3: - current_type += 1 - if catch_rate[pokeball-1] < 0.9 and items_stock[current_type] > 0: - # if current ball chance to catch is under 90%, and player has better ball - then use it - pokeball = current_type # use better ball - else: - # If we have a lot of berries (than the great ball), we prefer use a berry first! - if catch_rate[pokeball-1] < 0.42 and items_stock[pokeball+1]+30 < berries_count: - # If it's not the VIP type, we don't want to waste our ultra ball if no balls left. - if items_stock[1] == 0 and items_stock[2] == 0: - break - - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - self.emit_event( - 'pokemon_catch_rate', - level='debug', - formatted="Catch rate of {catch_rate} is low. Maybe will throw {berry_name} ({berry_count} left)", - data={ - 'catch_rate': success_percentage, - 'berry_name': self.item_list[str(berry_id)], - 'berry_count': berries_count-1 - } - ) - 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']: - catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - berries_count = berries_count -1 - berry_used = True - self.emit_event( - 'threw_berry', - formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", - data={ - "berry_name": self.item_list[str(berry_id)], - "new_catch_rate": success_percentage - } - ) - else: - if response_dict['status_code'] is 1: - self.emit_event( - 'softban', - level='warning', - formatted='Failed to use berry. You may be softbanned.' - ) - else: - self.emit_event( - 'threw_berry_failed', - formatted='Unknown response when throwing berry: {status_code}.', - data={ - 'status_code': response_dict['status_code'] - } - ) - - else: - #We don't have many berry to waste, pick a good ball first. Save some berry for future VIP pokemon - current_type = pokeball - while current_type < 2: - current_type += 1 - if catch_rate[pokeball-1] < 0.35 and items_stock[current_type] > 0: - # if current ball chance to catch is under 35%, and player has better ball - then use it - pokeball = current_type # use better ball - - #if the rate is still low and we didn't throw a berry before use berry - if catch_rate[pokeball-1] < 0.35 and berries_count > 0 and berry_used == False: - # If it's not the VIP type, we don't want to waste our ultra ball if no balls left. - if items_stock[1] == 0 and items_stock[2] == 0: - break - - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - self.emit_event( - 'pokemon_catch_rate', - level='debug', - formatted="Catch rate of {catch_rate} is low. Throwing {berry_name} ({berry_count} left)", - data={ - 'catch_rate': success_percentage, - 'berry_name': self.item_list[str(berry_id)], - 'berry_count': berries_count-1 - } - ) - 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']: - catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - berries_count = berries_count -1 - berry_used = True - self.emit_event( - 'threw_berry', - formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", - data={ - "berry_name": self.item_list[str(berry_id)], - "new_catch_rate": success_percentage - } - ) - else: - if response_dict['status_code'] is 1: - self.emit_event( - 'softban', - level='warning', - formatted='Failed to use berry. You may be softbanned.' - ) - else: - self.emit_event( - 'threw_berry_failed', - formatted='Unknown response when throwing berry: {status_code}.', - data={ - 'status_code': response_dict['status_code'] - } - ) - - # Re-check if berry is used, find a ball for a good capture rate - current_type=pokeball - while current_type < 2: - current_type += 1 - if catch_rate[pokeball-1] < 0.35 and items_stock[current_type] > 0: - pokeball = current_type # use better ball - - # This is to avoid rare case that a berry has ben throwed <0.42 - # and still picking normal pokeball (out of stock) -> error - if items_stock[1] == 0 and items_stock[2] > 0: - pokeball = 2 - - # Add this logic to avoid Pokeball = 0, Great Ball = 0, Ultra Ball = X - # And this logic saves Ultra Balls if it's a weak trash pokemon - if catch_rate[pokeball-1]<0.30 and items_stock[3]>0: - pokeball = 3 - - items_stock[pokeball] -= 1 - success_percentage = '{0:.2f}'.format(catch_rate[pokeball - 1] * 100) - self.emit_event( - 'threw_pokeball', - formatted='Used {pokeball}, with chance {success_percentage} ({count_left} left)', - data={ - 'pokeball': self.item_list[str(pokeball)], - 'success_percentage': success_percentage, - 'count_left': 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) - - response_dict = 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 - ) - - if response_dict and \ - 'responses' in response_dict and \ - 'CATCH_POKEMON' in response_dict['responses'] and \ - 'status' in response_dict['responses']['CATCH_POKEMON']: - status = response_dict['responses'][ - 'CATCH_POKEMON']['status'] - if status is 2: - self.emit_event( - 'pokemon_escaped', - formatted="{pokemon} escaped.", - data={'pokemon': pokemon_name} - ) - sleep(2) - continue - if status is 3: - self.emit_event( - 'pokemon_vanished', - formatted="{pokemon} vanished!", - data={'pokemon': pokemon_name} - ) - if success_percentage == 100: - self.softban = True - if status is 1: - self.bot.metrics.captured_pokemon(pokemon_name, cp, iv_display, pokemon_potential) - - self.emit_event( - 'pokemon_caught', - formatted='Captured {pokemon}! [CP {cp}] [Potential {iv}] [{iv_display}] [+{exp} exp]', - data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': pokemon_potential, - 'iv_display': iv_display, - 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) - } - ) - self.bot.softban = False - - if (self.config.evolve_captured - and (self.config.evolve_captured[0] == 'all' - or pokemon_name in self.config.evolve_captured)): - id_list2 = self.count_pokemon_inventory() - # No need to capture this even for metrics, player stats includes it. - pokemon_to_transfer = list(set(id_list2) - set(id_list1)) - - # TODO dont throw RuntimeError, do something better - if len(pokemon_to_transfer) == 0: - raise RuntimeError( - 'Trying to evolve 0 pokemons!') - response_dict = self.api.evolve_pokemon(pokemon_id=pokemon_to_transfer[0]) - status = response_dict['responses']['EVOLVE_POKEMON']['result'] - if status == 1: - self.emit_event( - 'pokemon_evolved', - formatted="{pokemon} evolved!", - data={'pokemon': pokemon_name} - ) - else: - self.emit_event( - 'pokemon_evolve_fail', - formatted="Failed to evolve {pokemon}!", - data={'pokemon': pokemon_name} - ) - break - time.sleep(5) - - def count_pokemon_inventory(self): - # don't use cached bot.get_inventory() here - # because we need to have actual information in capture logic - response_dict = self.api.get_inventory() - - id_list = [] - callback = lambda pokemon: id_list.append(pokemon['id']) - self._foreach_pokemon_in_inventory(response_dict, callback) - return id_list - - def _foreach_pokemon_in_inventory(self, response_dict, callback): + return False try: - reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) + responses = response_dict['responses'] + response = responses[self.response_key] + if response[self.response_status_key] != ENCOUNTER_STATUS_SUCCESS: + if response[self.response_status_key] == ENCOUNTER_STATUS_NOT_IN_RANGE: + self.emit_event('pokemon_not_in_range', formatted='Pokemon went out of range!') + elif response[self.response_status_key] == ENCOUNTER_STATUS_POKEMON_INVENTORY_FULL: + self.emit_event('pokemon_inventory_full', formatted='Your Pokemon inventory is full! Could not catch!') + return False except KeyError: - pass - else: - for item in response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data"], item) - except KeyError: - pass - else: - pokemon = item['inventory_item_data']['pokemon_data'] - if not pokemon.get('is_egg', False): - callback(pokemon) - - def pokemon_potential(self, pokemon_data): - total_iv = 0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - - for individual_stat in iv_stats: - try: - total_iv += pokemon_data[individual_stat] - except: - pokemon_data[individual_stat] = 0 - continue - - return round((total_iv / 45.0), 2) - - def should_capture_pokemon(self, pokemon_name, cp, iv, response_dict): - catch_config = self._get_catch_config_for(pokemon_name) - cp_iv_logic = catch_config.get('logic') - if not cp_iv_logic: - cp_iv_logic = self._get_catch_config_for('any').get('logic', 'and') - - catch_results = { - 'cp': False, - 'iv': False, - } - - if catch_config.get('never_catch', False): return False - if catch_config.get('always_catch', False): - return True - - catch_cp = catch_config.get('catch_above_cp', 0) - if cp > catch_cp: - catch_results['cp'] = True + # get pokemon data + pokemon_data = response['wild_pokemon']['pokemon_data'] if 'wild_pokemon' in response else response['pokemon_data'] + pokemon = Pokemon(self.pokemon_list, pokemon_data) - catch_iv = catch_config.get('catch_above_iv', 0) - if iv > catch_iv: - catch_results['iv'] = True - - logic_to_function = { - 'or': lambda x, y: x or y, - 'and': lambda x, y: x and y - } + # skip ignored pokemon + if not self._should_catch_pokemon(pokemon): + return False - return logic_to_function[cp_iv_logic](*catch_results.values()) + # log encounter + self.emit_event( + 'pokemon_appeared', + formatted='A wild {pokemon} appeared! [CP {cp}] [Potential {iv}] [A/D/S {iv_display}]', + data={ + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv, + 'iv_display': pokemon.iv_display, + } + ) + + # simulate app + sleep(3) + + # check for VIP pokemon + is_vip = self._is_vip_pokemon(pokemon) + if is_vip: + self.emit_event('vip_pokemon', formatted='This is a VIP pokemon. Catch!!!') + + # catch that pokemon! + encounter_id = self.pokemon['encounter_id'] + catch_rate_by_ball = [0] + response['capture_probability']['capture_probability'] # offset so item ids match indces + self._do_catch(pokemon, encounter_id, catch_rate_by_ball, is_vip=is_vip) - def _get_catch_config_for(self, pokemon): - catch_config = self.config.catch.get(pokemon) - if not catch_config: - catch_config = self.config.catch.get('any') - return catch_config + # simulate app + time.sleep(5) def create_encounter_api_call(self): encounter_id = self.pokemon['encounter_id'] @@ -491,29 +146,273 @@ def create_encounter_api_call(self): ) return request.call() - def check_vip_pokemon(self,pokemon, cp, iv): + ############################################################################ + # helpers + ############################################################################ + + def _pokemon_matches_config(self, config, pokemon, default_logic='and'): + pokemon_config = config.get(pokemon.name, config.get('any')) + + if not pokemon_config: + return False - vip_name = self.config.vips.get(pokemon) - if vip_name == {}: - return True - else: - catch_config = self.config.vips.get("any") - if not catch_config: - return False - cp_iv_logic = catch_config.get('logic', 'or') catch_results = { 'cp': False, 'iv': False, } - catch_cp = catch_config.get('catch_above_cp', 0) - if cp > catch_cp: + if pokemon_config.get('never_catch', False): + return False + + if pokemon_config.get('always_catch', False): + return True + + catch_cp = pokemon_config.get('catch_above_cp', 0) + if pokemon.cp > catch_cp: catch_results['cp'] = True - catch_iv = catch_config.get('catch_above_iv', 0) - if iv > catch_iv: + + catch_iv = pokemon_config.get('catch_above_iv', 0) + if pokemon.iv > catch_iv: catch_results['iv'] = True - logic_to_function = { - 'or': lambda x, y: x or y, - 'and': lambda x, y: x and y - } - return logic_to_function[cp_iv_logic](*catch_results.values()) + + return LOGIC_TO_FUNCTION[pokemon_config.get('logic', default_logic)](*catch_results.values()) + + def _should_catch_pokemon(self, pokemon): + return self._pokemon_matches_config(self.config.catch, pokemon) + + def _is_vip_pokemon(self, pokemon): + # having just a name present in the list makes them vip + if self.config.vips.get(pokemon.name) == {}: + return True + return self._pokemon_matches_config(self.config.vips, pokemon, default_logic='or') + + def _get_current_pokemon_ids(self): + # don't use cached bot.get_inventory() here because we need to have actual information in capture logic + response_dict = self.api.get_inventory() + + try: + inventory_items = response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] + except KeyError: + return [] # no items + + id_list = [] + for item in inventory_items: + try: + pokemon = item['inventory_item_data']['pokemon_data'] + except KeyError: + continue + + # ignore eggs + if pokemon.get('is_egg'): + continue + + id_list.append(pokemon['id']) + + return id_list + + def _pct(self, rate_by_ball): + return '{0:.2f}'.format(rate_by_ball * 100) + + def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball): + new_catch_rate_by_ball = [] + self.emit_event( + 'pokemon_catch_rate', + level='debug', + formatted='Catch rate of {catch_rate} with {ball_name} is low. Throwing {berry_name} (have {berry_count})', + data={ + 'catch_rate': self._pct(catch_rate_by_ball[current_ball]), + 'ball_name': self.item_list[str(current_ball)], + 'berry_name': self.item_list[str(berry_id)], + 'berry_count': berry_count + } + ) + + response_dict = self.api.use_item_capture( + item_id=berry_id, + encounter_id=encounter_id, + spawn_point_id=self.spawn_point_guid + ) + responses = response_dict['responses'] + + if response_dict and response_dict['status_code'] == 1: + + # update catch rates using multiplier + if 'item_capture_mult' in responses['USE_ITEM_CAPTURE']: + for rate in catch_rate_by_ball: + new_catch_rate_by_ball.append(rate * responses['USE_ITEM_CAPTURE']['item_capture_mult']) + self.emit_event( + 'threw_berry', + formatted="Threw a {berry_name}! Catch rate with {ball_name} is now: {new_catch_rate}", + data={ + 'berry_name': self.item_list[str(berry_id)], + 'ball_name': self.item_list[str(current_ball)], + 'new_catch_rate': self._pct(catch_rate_by_ball[current_ball]) + } + ) + + # softban? + else: + self.emit_event( + 'softban', + level='warning', + formatted='Failed to use berry. You may be softbanned.' + ) + + # unknown status code + else: + self.emit_event( + 'threw_berry_failed', + formatted='Unknown response when throwing berry: {status_code}.', + data={ + 'status_code': response_dict['status_code'] + } + ) + + return new_catch_rate_by_ball + + def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): + # settings that may be exposed at some point + berry_id = ITEM_RAZZBERRY + maximum_ball = ITEM_ULTRABALL if is_vip else ITEM_GREATBALL + ideal_catch_rate_before_throw = 0.9 if is_vip else 0.35 + + berry_count = self.bot.item_inventory_count(berry_id) + items_stock = self.bot.current_inventory() + + while True: + + # find lowest available ball + current_ball = ITEM_POKEBALL + while items_stock[current_ball] == 0 and current_ball < maximum_ball: + current_ball += 1 + if items_stock[current_ball] == 0: + self.emit_event('no_pokeballs', formatted='No usable pokeballs found!') + break + + # check future ball count + num_next_balls = 0 + next_ball = current_ball + while next_ball < maximum_ball: + next_ball += 1 + num_next_balls += items_stock[next_ball] + + # check if we've got berries to spare + berries_to_spare = berry_count > 0 if is_vip else berry_count > num_next_balls + 30 + + # use a berry if we are under our ideal rate and have berries to spare + used_berry = False + if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and berries_to_spare: + catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) + berry_count -= 1 + used_berry = True + + # pick the best ball to catch with + best_ball = current_ball + while best_ball < maximum_ball: + best_ball += 1 + if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and items_stock[best_ball] > 0: + # if current ball chance to catch is under our ideal rate, and player has better ball - then use it + current_ball = best_ball + + # if the rate is still low and we didn't throw a berry before, throw one + if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and berry_count > 0 and not used_berry: + catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) + berry_count -= 1 + + # get current pokemon list before catch + pokemon_before_catch = self._get_current_pokemon_ids() + + # try to catch pokemon! + items_stock[current_ball] -= 1 + self.emit_event( + 'threw_pokeball', + formatted='Used {ball_name}, with chance {success_percentage} ({count_left} left)', + data={ + 'ball_name': self.item_list[str(current_ball)], + 'success_percentage': self._pct(catch_rate_by_ball[current_ball]), + 'count_left': items_stock[current_ball] + } + ) + + reticle_size_parameter = normalized_reticle_size(self.config.catch_randomize_reticle_factor) + spin_modifier_parameter = spin_modifier(self.config.catch_randomize_spin_factor) + + response_dict = self.api.catch_pokemon( + encounter_id=encounter_id, + pokeball=current_ball, + 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 + ) + + try: + catch_pokemon_status = response_dict['responses']['CATCH_POKEMON']['status'] + except KeyError: + break + + # retry failed pokemon + if catch_pokemon_status == CATCH_STATUS_FAILED: + self.emit_event( + 'pokemon_capture_failed', + formatted='{pokemon} capture failed.. trying again!', + data={'pokemon': pokemon.name} + ) + sleep(2) + continue + + # abandon if pokemon vanished + elif catch_pokemon_status == CATCH_STATUS_VANISHED: + self.emit_event( + 'pokemon_vanished', + formatted='{pokemon} vanished!', + data={'pokemon': pokemon.name} + ) + if self._pct(catch_rate_by_ball[current_ball]) == 100: + self.bot.softban = True + + # pokemon caught! + elif catch_pokemon_status == CATCH_STATUS_SUCCESS: + self.bot.metrics.captured_pokemon(pokemon.name, pokemon.cp, pokemon.iv_display, pokemon.iv) + self.emit_event( + 'pokemon_caught', + formatted='Captured {pokemon}! [CP {cp}] [Potential {iv}] [{iv_display}] [+{exp} exp]', + data={ + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv, + 'iv_display': pokemon.iv_display, + 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) + } + ) + self.bot.softban = False + + # evolve pokemon if necessary + if self.config.evolve_captured and (self.config.evolve_captured[0] == 'all' or pokemon.name in self.config.evolve_captured): + pokemon_after_catch = self._get_current_pokemon_ids() + pokemon_to_evolve = list(set(pokemon_after_catch) - set(pokemon_before_catch)) + + if len(pokemon_to_evolve) == 0: + break + + self._do_evolve(pokemon, pokemon_to_evolve[0]) + + break + + def _do_evolve(self, pokemon, new_pokemon_id): + response_dict = self.api.evolve_pokemon(pokemon_id=new_pokemon_id) + catch_pokemon_status = response_dict['responses']['EVOLVE_POKEMON']['result'] + + if catch_pokemon_status == 1: + self.emit_event( + 'pokemon_evolved', + formatted='{pokemon} evolved!', + data={'pokemon': pokemon.name} + ) + else: + self.emit_event( + 'pokemon_evolve_fail', + formatted='Failed to evolve {pokemon}!', + data={'pokemon': pokemon.name} + ) From 597196e4af4484a192bdb972f0f4781ecd91731e Mon Sep 17 00:00:00 2001 From: devn0ll Date: Tue, 9 Aug 2016 02:17:23 +0200 Subject: [PATCH 086/202] Added Run-Loop (#3143) * Add files via upload modified run script wich let you run the boot in a loop(if it crashes it restarts) * Integreated Loop into run.sh modified run.sh to loop the script so that even if it crashes it automaticly restarts. --- run.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/run.sh b/run.sh index ec95acb3e3..d7ef7079b7 100755 --- a/run.sh +++ b/run.sh @@ -15,4 +15,12 @@ else fi fi +while [ true ] +do +echo "###############################################" +echo "##Exit two times with [Ctl+C] to end the loop##" +echo "###############################################" +sleep 1 python pokecli.py --config ${config} +sleep "10" +done From ec3babc8d7218b7a92913a04ced597b605365b79 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Tue, 9 Aug 2016 02:27:21 +0200 Subject: [PATCH 087/202] fixing loop in spin fort task (#3165) --- pokemongo_bot/cell_workers/spin_fort.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index e04a86dbc6..2b1c862fc9 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -139,6 +139,11 @@ def work(self): def get_fort_in_range(self): forts = self.bot.get_forts(order_by_distance=True) + for fort in forts: + if 'cooldown_complete_timestamp_ms' in fort: + self.bot.fort_timeouts[fort["id"]] = fort['cooldown_complete_timestamp_ms'] + forts.remove(fort) + forts = filter(lambda x: x["id"] not in self.bot.fort_timeouts, forts) if len(forts) == 0: From a2b9fe9be9642612420612795a15bc691189f8c9 Mon Sep 17 00:00:00 2001 From: Guru Prasad Date: Mon, 8 Aug 2016 20:28:38 -0400 Subject: [PATCH 088/202] Some love for the vim users (#3154) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 70ab34b250..4721ce0253 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,6 @@ include/ # Pip check file pip-selfcheck.json + +# Some love for the vim users +.*.sw* From 8a2a52bad0b381f265f0896e69c50566e11da67c Mon Sep 17 00:00:00 2001 From: Jack Venberg Date: Mon, 8 Aug 2016 21:18:57 -0700 Subject: [PATCH 089/202] Updated README with link to desktop version (#3208) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 171ee4f355..fd99669e74 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ PokemonGo bot is a project created by the [PokemonGoF](https://github.com/PokemonGoF) team. The project is currently setup in two main branches. `dev` and `master`. +## Help Needed on [Desktop Version](https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) + ## Please submit PR to [Dev branch](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) From 5e3179b9c22d355f5f324751d1e5d6df7c2b1148 Mon Sep 17 00:00:00 2001 From: Kraig Amador Date: Mon, 8 Aug 2016 21:20:47 -0700 Subject: [PATCH 090/202] Fix for #3190 (#3197) --- CONTRIBUTORS.md | 1 + pokemongo_bot/cell_workers/pokemon_catch_worker.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 02aaacab4f..89011f7c38 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -55,3 +55,4 @@ * Moonlight-Angel * mjmadsen * nikofil + * bigkraig diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index a0711b49d6..ae55b7d704 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -252,6 +252,7 @@ def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, cu # softban? else: + new_catch_rate_by_ball = catch_rate_by_ball self.emit_event( 'softban', level='warning', @@ -260,6 +261,7 @@ def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, cu # unknown status code else: + new_catch_rate_by_ball = catch_rate_by_ball self.emit_event( 'threw_berry_failed', formatted='Unknown response when throwing berry: {status_code}.', From e4c54dc593b7202ac95ebd70cfc672e88eb878ee Mon Sep 17 00:00:00 2001 From: pmquan Date: Tue, 9 Aug 2016 00:25:42 -0700 Subject: [PATCH 091/202] MoveToMap: Add minimum balls to run (#3166) --- configs/config.json.map.example | 1 + pokemongo_bot/cell_workers/move_to_map_pokemon.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/configs/config.json.map.example b/configs/config.json.map.example index 1079c999f9..43196196f5 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -58,6 +58,7 @@ "address": "http://localhost:5000", "max_distance": 500, "min_time": 60, + "min_ball": 50, "prioritize_vips": true, "snipe": false, "update_map": true, diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 9ab6bbb7a2..c97b240196 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -82,6 +82,7 @@ def initialize(self): self.pokemon_data = self.bot.pokemon_list self.unit = self.bot.config.distance_unit self.caught = [] + self.min_ball = self.config.get('min_ball', 1) data_file = 'data/map-caught-{}.json'.format(self.bot.config.username) if os.path.isfile(data_file): @@ -250,7 +251,7 @@ def work(self): pokemon = pokemon_list[0] # if we only have ultraballs and the target is not a vip don't snipe/walk - if (pokeballs + superballs) < 1 and not pokemon['is_vip']: + if (pokeballs + superballs) < self.min_ball and not pokemon['is_vip']: return WorkerResult.SUCCESS if self.config['snipe']: From 61b6854c8c78789c7ee3343df48cb96b25f229a9 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Tue, 9 Aug 2016 09:40:52 +0200 Subject: [PATCH 092/202] added config to ignore item count for Spin and MoveToFort (#3160) --- pokemongo_bot/cell_workers/move_to_fort.py | 9 +++++---- pokemongo_bot/cell_workers/spin_fort.py | 8 +++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index e4b4187d20..7dcd0977b1 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -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) diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 2b1c862fc9..445946e7e1 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -15,14 +15,16 @@ class SpinFort(BaseTask): SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): + self.ignore_item_count = self.config.get("ignore_item_count", False) + def should_run(self): if not self.bot.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 False - return True + return self.ignore_item_count or self.bot.has_space_for_loot() def work(self): fort = self.get_fort_in_range() From 03d7f92ebf127572337d5fab748a4bde1577a186 Mon Sep 17 00:00:00 2001 From: aeckert Date: Tue, 9 Aug 2016 02:18:31 -0600 Subject: [PATCH 093/202] [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 From f6d73af7d8c664673c3b18617404204a766e82d4 Mon Sep 17 00:00:00 2001 From: Eli White Date: Tue, 9 Aug 2016 01:20:26 -0700 Subject: [PATCH 094/202] Keeping a cache of gym information (#3236) --- pokemongo_bot/__init__.py | 4 +++- pokemongo_bot/gym_cache.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 pokemongo_bot/gym_cache.py diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 711ddd1721..6f45034f1a 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -25,6 +25,7 @@ from human_behaviour import sleep from item_list import Item from metrics import Metrics +from gym_cache import GymCache from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler from pokemongo_bot.socketio_server.runner import SocketIoRunner from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl @@ -61,6 +62,7 @@ def __init__(self, config): ) self.item_list = json.load(open(os.path.join('data', 'items.json'))) self.metrics = Metrics(self) + self.gym_cache = GymCache(self) self.latest_inventory = None self.cell = None self.recent_forts = [None] * config.forts_max_circle_size @@ -499,7 +501,7 @@ 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: - response_gym_details = self.api.get_gym_details( + response_gym_details = self.gym_cache.get( gym_id=fort.get('id'), player_latitude=lng, player_longitude=lat, diff --git a/pokemongo_bot/gym_cache.py b/pokemongo_bot/gym_cache.py new file mode 100644 index 0000000000..b7bc7eb1eb --- /dev/null +++ b/pokemongo_bot/gym_cache.py @@ -0,0 +1,32 @@ +import time + +class GymCache(object): + def __init__(self, bot): + self.bot = bot + self.cache = {} + self.cache_length_seconds = 60 * 10 + + def get(self, gym_id, player_latitude, player_longitude, gym_latitude, gym_longitude): + if gym_id not in self.cache: + response_gym_details = self.bot.api.get_gym_details( + gym_id=gym_id, + player_latitude=player_latitude, + player_longitude=player_longitude, + gym_latitude=gym_latitude, + gym_longitude=gym_longitude + ) + + self.cache[gym_id] = response_gym_details + + gym_info = self.cache[gym_id] + gym_info['last_accessed'] = time.time() + + self._remove_stale_gyms() + return gym_info + + def _remove_stale_gyms(self): + for gym_id, gym_details in self.cache.items(): + if gym_details['last_accessed'] < time.time() - self.cache_length_seconds: + del self.cache[gym_id] + + From ce9eb2b54f9c0264af07a19a6b0a379ea484c78e Mon Sep 17 00:00:00 2001 From: aceradryd Date: Tue, 9 Aug 2016 10:37:31 +0200 Subject: [PATCH 095/202] New Option: "dont_nickname_favorite" (#2496) * New Option: "dont_nickname_favorite" This change (line 19) adds the option, that the user can choose, whether their favorite pokemons should also get a new nickname or not. If a user want this, then he or she has to add the line ("dont_nickname_favorite" = true) after ("nickname_template": " ... ",). * Update nickname_pokemon.py * Update * Put change to line 30 This reduce the reduce the runtime, because favorite pokemon won't be added to the list. --- pokemongo_bot/cell_workers/nickname_pokemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py index cda206ad20..e521cadfba 100644 --- a/pokemongo_bot/cell_workers/nickname_pokemon.py +++ b/pokemongo_bot/cell_workers/nickname_pokemon.py @@ -27,7 +27,7 @@ def _get_inventory_pokemon(self,inventory_dict): except KeyError: pass else: - if not pokemon.get('is_egg',False): + if not pokemon.get('is_egg',False) and not (pokemon.get('favorite', 0) == 1 and self.config.get('dont_nickname_favorite',False)): pokemon_data.append(pokemon) return pokemon_data From 7e699dd8b0d2ddfcd4b1b9ea8757d139f20f44d0 Mon Sep 17 00:00:00 2001 From: Eli White Date: Tue, 9 Aug 2016 02:08:01 -0700 Subject: [PATCH 096/202] Restart the loop when catching pokemon and there are more to catch (#3242) --- .../cell_workers/catch_visible_pokemon.py | 28 ++++++++++++++++--- .../cell_workers/pokemon_catch_worker.py | 10 +++---- pokemongo_bot/worker_result.py | 1 + 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py index 1bfed225df..654c2467b3 100644 --- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -3,13 +3,24 @@ from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker from utils import distance +from pokemongo_bot.worker_result import WorkerResult class CatchVisiblePokemon(BaseTask): SUPPORTED_TASK_API_VERSION = 1 def work(self): - if 'catchable_pokemons' in self.bot.cell and len(self.bot.cell['catchable_pokemons']) > 0: + num_catchable_pokemon = 0 + if 'catchable_pokemons' in self.bot.cell: + num_catchable_pokemon = len(self.bot.cell['catchable_pokemons']) + + num_wild_pokemon = 0 + if 'wild_pokemons' in self.bot.cell: + num_wild_pokemon = len(self.bot.cell['wild_pokemons']) + + num_available_pokemon = num_catchable_pokemon + num_wild_pokemon + + if num_catchable_pokemon > 0: # Sort all by distance from current pos- eventually this should # build graph & A* it self.bot.cell['catchable_pokemons'].sort( @@ -33,15 +44,24 @@ def work(self): } ) - return self.catch_pokemon(self.bot.cell['catchable_pokemons'].pop(0)) + self.catch_pokemon(self.bot.cell['catchable_pokemons'].pop(0)) + if num_catchable_pokemon > 1: + return WorkerResult.RUNNING + else: + return WorkerResult.SUCCESS - if 'wild_pokemons' in self.bot.cell and len(self.bot.cell['wild_pokemons']) > 0: + if num_available_pokemon > 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)) + self.catch_pokemon(self.bot.cell['wild_pokemons'].pop(0)) + + if num_catchable_pokemon > 1: + return WorkerResult.RUNNING + else: + return WorkerResult.SUCCESS def catch_pokemon(self, pokemon): worker = PokemonCatchWorker(pokemon, self.bot) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 207ed83d81..d551a68632 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -4,7 +4,7 @@ from pokemongo_bot import inventory from pokemongo_bot.base_task import BaseTask from pokemongo_bot.human_behaviour import normalized_reticle_size, sleep, spin_modifier - +from pokemongo_bot.worker_result import WorkerResult CATCH_STATUS_SUCCESS = 1 CATCH_STATUS_FAILED = 2 @@ -68,7 +68,7 @@ def work(self, response_dict=None): # validate response if not response_dict: - return False + return WorkerResult.ERROR try: responses = response_dict['responses'] response = responses[self.response_key] @@ -77,9 +77,9 @@ def work(self, response_dict=None): self.emit_event('pokemon_not_in_range', formatted='Pokemon went out of range!') elif response[self.response_status_key] == ENCOUNTER_STATUS_POKEMON_INVENTORY_FULL: self.emit_event('pokemon_inventory_full', formatted='Your Pokemon inventory is full! Could not catch!') - return False + return WorkerResult.ERROR except KeyError: - return False + return WorkerResult.ERROR # get pokemon data pokemon_data = response['wild_pokemon']['pokemon_data'] if 'wild_pokemon' in response else response['pokemon_data'] @@ -87,7 +87,7 @@ def work(self, response_dict=None): # skip ignored pokemon if not self._should_catch_pokemon(pokemon): - return False + return WorkerResult.SUCCESS # log encounter self.emit_event( diff --git a/pokemongo_bot/worker_result.py b/pokemongo_bot/worker_result.py index f38ceb9704..0e3ba10ebd 100644 --- a/pokemongo_bot/worker_result.py +++ b/pokemongo_bot/worker_result.py @@ -1,3 +1,4 @@ class WorkerResult(object): RUNNING = 'RUNNING' SUCCESS = 'SUCCESS' + ERROR = 'ERROR' From e73d302a13dac88b4e6a41553b8419e76287957c Mon Sep 17 00:00:00 2001 From: Andreas Schubert Date: Tue, 9 Aug 2016 11:19:03 +0200 Subject: [PATCH 097/202] fixed NameError: global name 'pokemon_name' is not defined (#3244) resolves ```traceback (most recent call last): File "pokecli.py", line 521, in main() File "pokecli.py", line 95, in main bot.tick() File "/usr/src/app/pokemongo_bot/__init__.py", line 451, in tick if worker.work() == WorkerResult.RUNNING: File "/usr/src/app/pokemongo_bot/cell_workers/evolve_pokemon.py", line 38, in work self._execute_pokemon_evolve(pokemon, cache) File "/usr/src/app/pokemongo_bot/cell_workers/evolve_pokemon.py", line 117, in _execute_pokemon_evolve cache[pokemon.name] = 1 NameError: global name 'pokemon_name' is not defined``` --- pokemongo_bot/cell_workers/evolve_pokemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index d045fc8fef..4cc451115b 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -114,7 +114,7 @@ def _execute_pokemon_evolve(self, pokemon, cache): return True else: # cache pokemons we can't evolve. Less server calls - cache[pokemon_name] = 1 + cache[pokemon.name] = 1 sleep(0.7) return False From 8203f360feeccd61387e5e3e52c4779c9fdc3e1c Mon Sep 17 00:00:00 2001 From: Eli White Date: Tue, 9 Aug 2016 02:19:31 -0700 Subject: [PATCH 098/202] Stop fetching gym details (#3245) --- pokemongo_bot/__init__.py | 18 ------------------ pokemongo_bot/gym_cache.py | 32 -------------------------------- 2 files changed, 50 deletions(-) delete mode 100644 pokemongo_bot/gym_cache.py diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 6f45034f1a..f0f98ba157 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -25,7 +25,6 @@ from human_behaviour import sleep from item_list import Item from metrics import Metrics -from gym_cache import GymCache from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler from pokemongo_bot.socketio_server.runner import SocketIoRunner from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl @@ -62,7 +61,6 @@ def __init__(self, config): ) self.item_list = json.load(open(os.path.join('data', 'items.json'))) self.metrics = Metrics(self) - self.gym_cache = GymCache(self) self.latest_inventory = None self.cell = None self.recent_forts = [None] * config.forts_max_circle_size @@ -496,22 +494,6 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): location = self.position[0:2] cells = self.find_close_cells(*location) - # insert detail info about gym to fort - for cell in cells: - if 'forts' in cell: - for fort in cell['forts']: - if fort.get('type') != 1: - response_gym_details = self.gym_cache.get( - gym_id=fort.get('id'), - player_latitude=lng, - player_longitude=lat, - gym_latitude=fort.get('latitude'), - gym_longitude=fort.get('longitude') - ) - fort['gym_details'] = response_gym_details.get( - 'responses', {} - ).get('GET_GYM_DETAILS', None) - user_data_cells = "data/cells-%s.json" % self.config.username with open(user_data_cells, 'w') as outfile: json.dump(cells, outfile) diff --git a/pokemongo_bot/gym_cache.py b/pokemongo_bot/gym_cache.py deleted file mode 100644 index b7bc7eb1eb..0000000000 --- a/pokemongo_bot/gym_cache.py +++ /dev/null @@ -1,32 +0,0 @@ -import time - -class GymCache(object): - def __init__(self, bot): - self.bot = bot - self.cache = {} - self.cache_length_seconds = 60 * 10 - - def get(self, gym_id, player_latitude, player_longitude, gym_latitude, gym_longitude): - if gym_id not in self.cache: - response_gym_details = self.bot.api.get_gym_details( - gym_id=gym_id, - player_latitude=player_latitude, - player_longitude=player_longitude, - gym_latitude=gym_latitude, - gym_longitude=gym_longitude - ) - - self.cache[gym_id] = response_gym_details - - gym_info = self.cache[gym_id] - gym_info['last_accessed'] = time.time() - - self._remove_stale_gyms() - return gym_info - - def _remove_stale_gyms(self): - for gym_id, gym_details in self.cache.items(): - if gym_details['last_accessed'] < time.time() - self.cache_length_seconds: - del self.cache[gym_id] - - From 4c95259c25543b483a539a928ae67e7e590778f3 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Tue, 9 Aug 2016 11:20:20 +0200 Subject: [PATCH 099/202] Checking all forts for lured pokemon (#3163) --- .../cell_workers/catch_lured_pokemon.py | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/pokemongo_bot/cell_workers/catch_lured_pokemon.py b/pokemongo_bot/cell_workers/catch_lured_pokemon.py index 10a046dce9..187c1eb40d 100644 --- a/pokemongo_bot/cell_workers/catch_lured_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_lured_pokemon.py @@ -1,9 +1,11 @@ # -*- 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.worker_result import WorkerResult +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): @@ -11,24 +13,42 @@ class CatchLuredPokemon(BaseTask): def work(self): lured_pokemon = self.get_lured_pokemon() - if lured_pokemon: - self.catch_pokemon(lured_pokemon) + if len(lured_pokemon) > 0: + self.catch_pokemon(lured_pokemon[0]) + + if len(lured_pokemon) > 1: + return WorkerResult.RUNNING + + return WorkerResult.SUCCESS def get_lured_pokemon(self): + forts_in_range = [] + pokemon_to_catch = [] forts = self.bot.get_forts(order_by_distance=True) if len(forts) == 0: 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) - if encounter_id: + 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'] + result = { 'encounter_id': encounter_id, 'fort_id': fort['id'], @@ -36,15 +56,14 @@ def get_lured_pokemon(self): '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) From d42082349736c83616d5c0d42acf43d8abb0b745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Phan=20H=E1=BA=A3i=20Phong?= Date: Tue, 9 Aug 2016 16:48:29 +0700 Subject: [PATCH 100/202] Fix flooding of keep_best_release (#3223) * Fix flooding of keep_best_release * Fix flooding of keep_best_release --- .../cell_workers/transfer_pokemon.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 5c1d30fae7..ebc197ef24 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -43,17 +43,6 @@ def work(self): all_pokemons.remove(pokemon) best_pokemons.append(pokemon) - if best_pokemons and all_pokemons: - self.emit_event( - 'keep_best_release', - formatted="Keeping best {amount} {pokemon}, based on {criteria}", - data={ - 'amount': len(best_pokemons), - 'pokemon': pokemon_name, - 'criteria': order_criteria - } - ) - transfer_pokemons = [pokemon for pokemon in all_pokemons if self.should_release_pokemon(pokemon_name, pokemon['cp'], @@ -61,6 +50,16 @@ def work(self): True)] if transfer_pokemons: + if best_pokemons: + self.emit_event( + 'keep_best_release', + formatted="Keeping best {amount} {pokemon}, based on {criteria}", + data={ + 'amount': len(best_pokemons), + 'pokemon': pokemon_name, + 'criteria': order_criteria + } + ) for pokemon in transfer_pokemons: self.release_pokemon(pokemon_name, pokemon['cp'], pokemon['iv'], pokemon['pokemon_data']['id']) else: From 0f9351e882a0b2f48c5d411e8ed9d990283e009c Mon Sep 17 00:00:00 2001 From: Nikhil Pandey Date: Tue, 9 Aug 2016 16:19:09 +0545 Subject: [PATCH 101/202] [Feature] Recycle Threshold (#2465) * Add Threshold Option * Add Threshold Option to Example Configs * Add Name to Contributors * Change config name and message * Remove logger * Add option to run when storage less than something * Change Message * Fix * Error fixes, message improvement * Config Changes and Remove Option --- CONTRIBUTORS.md | 1 + configs/config.json.cluster.example | 1 + configs/config.json.example | 1 + configs/config.json.map.example | 1 + configs/config.json.path.example | 1 + configs/config.json.pokemon.example | 1 + pokemongo_bot/__init__.py | 4 ++++ pokemongo_bot/cell_workers/recycle_items.py | 23 +++++++++++++++++++-- 8 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 89011f7c38..fd219fe470 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -56,3 +56,4 @@ * mjmadsen * nikofil * bigkraig + * nikhil-pandey diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index b32eb4f668..e4597519f6 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/configs/config.json.example b/configs/config.json.example index ec46c15eb9..d2e8d4f064 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/configs/config.json.map.example b/configs/config.json.map.example index 43196196f5..a729c57274 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -33,6 +33,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 94a9fdba07..dec7457246 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 1d428a6ae7..e7ba38dc37 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index f0f98ba157..dd4de22551 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -349,6 +349,10 @@ def _register_events(self): 'amount', 'item', 'maximum' ) ) + self.event_manager.register_event( + 'item_discard_skipped', + parameters=('space',) + ) self.event_manager.register_event( 'item_discard_fail', parameters=('item',) diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 2c969913b0..673b373fba 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -3,10 +3,12 @@ from pokemongo_bot.base_task import BaseTask from pokemongo_bot.tree_config_builder import ConfigException + class RecycleItems(BaseTask): SUPPORTED_TASK_API_VERSION = 1 def initialize(self): + self.min_empty_space = self.config.get('min_empty_space', None) self.item_filter = self.config.get('item_filter', {}) self._validate_item_filter() @@ -15,9 +17,26 @@ def _validate_item_filter(self): for config_item_name, bag_count in self.item_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)) + raise ConfigException( + "item {} does not exist, spelling mistake? (check for valid item names in data/items.json)".format( + config_item_name)) 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: + 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') @@ -58,7 +77,7 @@ def work(self): 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} + # {'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 From 49f9177ef4226a6a69499d7afc1fb24e5c72fc50 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Tue, 9 Aug 2016 05:53:28 -0500 Subject: [PATCH 102/202] Call heartbeat on step_walker even if speed is higher than distance (#2513) --- pokemongo_bot/step_walker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pokemongo_bot/step_walker.py b/pokemongo_bot/step_walker.py index 263699095d..f6c2cbfe96 100644 --- a/pokemongo_bot/step_walker.py +++ b/pokemongo_bot/step_walker.py @@ -39,6 +39,7 @@ def __init__(self, bot, speed, dest_lat, dest_lng): def step(self): if (self.dLat == 0 and self.dLng == 0) or self.dist < self.speed: self.api.set_position(self.destLat, self.destLng, 0) + self.bot.heartbeat() return True totalDLat = (self.destLat - self.initLat) From 59f55fd38f7eb0d2677c650b039b0d9277fceba2 Mon Sep 17 00:00:00 2001 From: Joao Poupino Date: Tue, 9 Aug 2016 12:30:00 +0100 Subject: [PATCH 103/202] Return an empty list if no pokemon are available. (#3259) The changes introduced in 4c95259 expose this bug. --- pokemongo_bot/cell_workers/catch_lured_pokemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/catch_lured_pokemon.py b/pokemongo_bot/cell_workers/catch_lured_pokemon.py index 187c1eb40d..afddeb53d5 100644 --- a/pokemongo_bot/cell_workers/catch_lured_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_lured_pokemon.py @@ -27,7 +27,7 @@ def get_lured_pokemon(self): forts = self.bot.get_forts(order_by_distance=True) if len(forts) == 0: - return False + return [] for fort in forts: distance_to_fort = distance( From 79266a0ccfedcf04b690d23d5373c95024f89681 Mon Sep 17 00:00:00 2001 From: mmns Date: Tue, 9 Aug 2016 15:21:27 +0200 Subject: [PATCH 104/202] Allow UpdateTitleStats to emit events instead of rewriting the console (#3264) --- pokemongo_bot/cell_workers/update_title_stats.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index acbfaa7fe4..0a6e3c592d 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -70,6 +70,8 @@ def __init__(self, bot, config): self.min_interval = self.DEFAULT_MIN_INTERVAL self.displayed_stats = self.DEFAULT_DISPLAYED_STATS + self.bot.event_manager.register_event('update_title', parameters=('title')) + self._process_config() def initialize(self): @@ -109,6 +111,15 @@ def _update_title(self, title, platform): :rtype: None :raise: RuntimeError: When the given platform isn't supported. """ + + self.emit_event( + 'update_title', + formatted="{title}", + data={ + 'title': title + } + ) + if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) stdout.flush() From b3d8d8650120798961681e7e25251c4839121b46 Mon Sep 17 00:00:00 2001 From: Eli White Date: Tue, 9 Aug 2016 06:22:07 -0700 Subject: [PATCH 105/202] Updating our issue and PR templates to be more helpful (#3262) --- .github/ISSUE_TEMPLATE.md | 12 ++++++++++-- .github/PULL_REQUEST_TEMPLATE.md | 18 +++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index a1d7168e75..e5a3f1f317 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,7 @@ -Please check configuration at http://jsonlint.com/ before posting an issue. +## Stop! Before you create this issue (you can delete this section when opening the issue): +1. Have you validated that your config.json is valid JSON? Use http://jsonlint.com/ to check. +2. Have you searched to see if there are other issues for the same issue? If so, comment on that issue instead. +3. Are you running `master`? We work on the `dev` branch and then add that functionality to `master`. Your issue may be fixed on `dev` and there is no need for this issue, just wait and it will eventually be merged to `master`. ### Expected Behavior @@ -6,10 +9,15 @@ Please check configuration at http://jsonlint.com/ before posting an issue. ### Actual Behavior +### Your config.json (remove your credentials and any other private info) +``` +your config here +``` + ### Steps to Reproduce ### Other Information -OS: +OS: Git Commit: (run 'git log -n 1 --pretty=format:"%H"' and paste it here) Python Version: (run 'python -V' and paste it here) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5178d928b5..a43df95477 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,15 @@ -Short Description: +# Please Note (you may remove this section before opening your PR): +We receive lots of PRs and it is hard to give proper review to PRs. Please make it easy on us by following these guidelines: -Fixes: -- -- -- +1. We do not accept changes to `master`. Please make sure your pull request is aimed at `dev`. +2. If you changed a bunch of files (that aren't config files) or multiple workers to implement your feature, it probably won't get proper attention. Please split it up into multiple, smaller, more focused, and iterative PRs if you can. +3. If you are adding a config value to something, make sure you update the appropriate `config.json` example files. + + +## Short Description: + +## Fixes (provide links to github issues if you can): +- +- +- From 1060afa55ee9caa1502df8a90f959d0d461cdb35 Mon Sep 17 00:00:00 2001 From: Raiven66 Date: Tue, 9 Aug 2016 15:27:08 +0200 Subject: [PATCH 106/202] Dev (#3277) * * adding enhanced sniping capabilities for move_to_map_pokemon * Adding enhanced sniping capabilities for move_to_map_pokemon --- configs/config.json.map.example | 4 +++- pokemongo_bot/cell_workers/move_to_map_pokemon.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/configs/config.json.map.example b/configs/config.json.map.example index a729c57274..bb6878f5ac 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -61,7 +61,9 @@ "min_time": 60, "min_ball": 50, "prioritize_vips": true, - "snipe": false, + "snipe": true, + "snipe_high_prio_only": true, + "snipe_high_prio_threshold": 400, "update_map": true, "mode": "priority", "catch": { diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index c97b240196..2cfd45d14b 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -255,7 +255,11 @@ def work(self): return WorkerResult.SUCCESS if self.config['snipe']: - return self.snipe(pokemon) + if self.config['snipe_high_prio_only']: + if self.config['snipe_high_prio_threshold'] < pokemon['priority'] or pokemon['is_vip']: + self.snipe(pokemon) + else: + return self.snipe(pokemon) step_walker = self._move_to(pokemon) if not step_walker.step(): From f4b4c282dcaa1a26676fbae5951f1278dd3c86ce Mon Sep 17 00:00:00 2001 From: Erik Weber Date: Tue, 9 Aug 2016 15:28:27 +0200 Subject: [PATCH 107/202] Update pgoapi to a newer version (#3241) This should hopefully fix issues like #3181, #3098, #2874 and potentially more. Needs testing/verification. I am running now, but it does take about an hour to trigger. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f6a22a0233..76b1d15a7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy==1.11.0 networkx==1.11 --e git+https://github.com/keyphact/pgoapi.git@249d3be7fbbdabc7f9adea17cbc899d6549e47a2#egg=pgoapi +-e git+https://github.com/keyphact/pgoapi.git@a2755eb42dfe49e359798d2f4defefc97fb8163d#egg=pgoapi geopy==1.11.0 protobuf==3.0.0b4 requests==2.10.0 From 3e1dc1b76f3ae5e134a17a3a974f865494d558d3 Mon Sep 17 00:00:00 2001 From: Eevee Date: Tue, 9 Aug 2016 22:29:02 +0900 Subject: [PATCH 108/202] Fix unexpected egg incubation retry (#3276) incubator['used'] flag is set but not used in IncubateEggs._apply_incubators --- pokemongo_bot/cell_workers/incubate_eggs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pokemongo_bot/cell_workers/incubate_eggs.py b/pokemongo_bot/cell_workers/incubate_eggs.py index 5761090ea5..805129435b 100644 --- a/pokemongo_bot/cell_workers/incubate_eggs.py +++ b/pokemongo_bot/cell_workers/incubate_eggs.py @@ -49,6 +49,8 @@ def work(self): def _apply_incubators(self): for incubator in self.ready_incubators: + if incubator.get('used', False): + continue for egg in self.eggs: if egg["used"] or egg["km"] == -1: continue From 0f2bddd26675c5232b2e9f4e83dc6525c41a842d Mon Sep 17 00:00:00 2001 From: Jordan Christensen Date: Tue, 9 Aug 2016 09:31:21 -0400 Subject: [PATCH 109/202] has_next_evolution is a function not a property (#3284) --- CONTRIBUTORS.md | 1 + pokemongo_bot/inventory.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index fd219fe470..6e0e44f5fd 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -57,3 +57,4 @@ * nikofil * bigkraig * nikhil-pandey + * thebigjc diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index 1cd8a893b2..c74a85296f 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -165,7 +165,7 @@ def __init__(self, data): self.iv = self._compute_iv() def can_evolve_now(self): - return self.has_next_evolution and self.candy_quantity > self.evolution_cost + 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 From 2ded2ee9c70bc4d6f7b2b3128535a81850cef2b2 Mon Sep 17 00:00:00 2001 From: Simon Shi Date: Wed, 10 Aug 2016 00:28:33 +0800 Subject: [PATCH 110/202] Powerful setup.sh (#3263) * Rewrite run.sh Very powerful run.sh with lots of function. 1.install(make .so) 2.update 3.config generator 4.config backup 5.run loop make it never down It should run like run.sh *.json or other opinion. See -help. * Update run.sh * Update run.sh OK problem solved * Delete setup.py * Rename run.sh to setup.sh * Create run.sh * Update setup.sh * Update install.sh * Update setup.sh * Update run.sh * Update setup.sh Some small fix. --- install.sh | 79 ++++++++++++++++++++++++------- run.sh | 33 ++++++------- setup.py | 13 ------ setup.sh | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 50 deletions(-) mode change 100755 => 100644 run.sh delete mode 100644 setup.py create mode 100755 setup.sh diff --git a/install.sh b/install.sh index c11992bb0c..32cc1e1124 100755 --- a/install.sh +++ b/install.sh @@ -1,19 +1,62 @@ #!/usr/bin/env bash - -# Setup Python virtualenv -echo "Setting up Python virtualenv..." -eval "virtualenv ." -eval "source bin/activate" -echo "Python virtualenv setup successfully." - -# Install pip requirements -echo "Installing pip requirements..." -eval "pip install -r requirements.txt" -echo "Installed pip requirements." -echo "Installing and updating git submodules..." - -# Install git submodules -eval "cd ./web && git submodule init && cd .." -eval "git submodule update" -echo "Done." -echo "Please create and setup configs/config.json. Then, run 'python pokecli.py --config ./configs/config.json' or './run.sh' on Mac/Linux" \ No newline at end of file +pokebotpath=$(pwd) +cd $pokebotpath +if [ -f /etc/debian_version ] +then +echo "You are on Debian/Ubuntu" +sudo apt-get update +sudo apt-get -y install python python-pip python-dev build-essential git python-protobuf virtualenv +elif [ -f /etc/redhat-release ] +then +echo "You are on CentOS/RedHat" +sudo yum -y install epel-release +sudo yum -y install python-pip +elif [ "$(uname -s)" == "Darwin" ] +then +echo "You are on Mac os" +sudo brew update +sudo brew install --devel protobuf +else +echo "Nothing happend." +fi +pip install virtualenv +cd $pokebotpath +git pull +git submodule init +git submodule foreach git pull origin master +virtualenv . +source bin/activate +pip install -r requirements.txt +echo "Start to make encrypt.so" +wget http://pgoapi.com/pgoencrypt.tar.gz +tar -xf pgoencrypt.tar.gz +cd pgoencrypt/src/ +make +mv libencrypt.so $pokebotpath/encrypt.so +cd ../.. +rm -rf pgoencrypt.tar.gz +rm -rf pgoencrypt +echo "Install complete." +cd $pokebotpath +read -p "1.google 2.ptc +" auth +read -p "Input username +" username +read -p "Input password +" -s password +read -p " +Input location +" location +read -p "Input gmapkey +" gmapkey +cp configs/config.json.example configs/config.json +if [ "$auth" = "2" ] +then +sed -i "s/google/ptc/g" configs/config.json +fi +sed -i "s/YOUR_USERNAME/$username/g" configs/config.json +sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json +sed -i "s/SOME_LOCATION/$location/g" configs/config.json +sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json +echo "Edit configs/config.json to modify any other config. Use run.sh to run." +exit 0 diff --git a/run.sh b/run.sh old mode 100755 new mode 100644 index d7ef7079b7..0812dfac8c --- a/run.sh +++ b/run.sh @@ -1,26 +1,21 @@ #!/usr/bin/env bash - -# Starts PokemonGo-Bot -config="" - +pokebotpath=$(pwd) +filename="" if [ ! -z $1 ]; then - config=$1 +filename=$1 else - config="./configs/config.json" - if [ ! -f ${config} ]; then - echo -e "There's no ./configs/config.json file" - echo -e "Please create one or use another config file" - echo -e "./run.sh [path/to/config/file]" - exit 1 - fi +filename="./configs/config.json" +if [ ! -f "$filename" ] +then +echo "There's no "$filename" file. use setup.sh -config to creat one." +fi fi -while [ true ] +while true do -echo "###############################################" -echo "##Exit two times with [Ctl+C] to end the loop##" -echo "###############################################" -sleep 1 -python pokecli.py --config ${config} -sleep "10" +cd $pokebotpath +python pokecli.py -cf $filename +read -p "Press any button or wait 20 seconds." -r -s -n1 -t 20 +echo `date`"Pokebot"$*" Stopped." done +exit 0 diff --git a/setup.py b/setup.py deleted file mode 100644 index 6ccf893c0d..0000000000 --- a/setup.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python - -from pip.req import parse_requirements - -install_reqs = parse_requirements("requirements.txt", session=False) - -reqs = [str(ir.req) for ir in install_reqs] - -setup(name='pgoapi', - version='1.0', - url='https://github.com/tejado/pgoapi', - packages=['pgoapi'], - install_requires=reqs) diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000000..bcff0feefb --- /dev/null +++ b/setup.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +pokebotpath=$(pwd) +backuppath=$(pwd)"/backup" + +function Pokebotupdate () { +cd $pokebotpath +git pull +git submodule init +git submodule foreach git pull origin master +virtualenv . +source bin/activate +pip install -r requirements.txt +} + +function Pokebotencrypt () { +echo "Start to make encrypt.so" +wget http://pgoapi.com/pgoencrypt.tar.gz +tar -xf pgoencrypt.tar.gz +cd pgoencrypt/src/ +make +mv libencrypt.so $pokebotpath/encrypt.so +cd ../.. +rm -rf pgoencrypt.tar.gz +rm -rf pgoencrypt +} + +function Pokebotconfig () { +cd $pokebotpath +read -p "1.google 2.ptc +" auth +read -p "Input username +" username +read -p "Input password +" -s password +read -p " +Input location +" location +read -p "Input gmapkey +" gmapkey +cp configs/config.json.example configs/config.json +if [ "$auth" = "2" ] +then +sed -i "s/google/ptc/g" configs/config.json +fi +sed -i "s/YOUR_USERNAME/$username/g" configs/config.json +sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json +sed -i "s/SOME_LOCATION/$location/g" configs/config.json +sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json +echo "Edit configs/config.json to modify any other config." +} + +function Pokebotinstall () { +cd $pokebotpath +if [ -f /etc/debian_version ] +then +echo "You are on Debian/Ubuntu" +sudo apt-get update +sudo apt-get -y install python python-pip python-dev build-essential git python-protobuf virtualenv +elif [ -f /etc/redhat-release ] +then +echo "You are on CentOS/RedHat" +sudo yum -y install epel-release +sudo yum -y install python-pip +elif [ "$(uname -s)" == "Darwin" ] +then +echo "You are on Mac os" +sudo brew update +sudo brew install --devel protobuf +else +echo "Nothing happend." +fi +sudo pip install virtualenv +Pokebotupdate +Pokebotencrypt +echo "Install complete." +Pokebotconfig +} + +function Pokebotreset () { +cd $pokebotpath +git fetch --all +git reset --hard origin/dev +Pokebotupdate +} + +function Pokebothelp () { +echo "usage:" +echo " -i,--install. Install PokemonGo-Bot." +echo " -b,--backup. Backup config files." +echo " -c,--config. Easy config generator." +echo " -e,--encrypt. Make encrypt.so." +echo " -r,--reset. Force sync dev branch." +echo " -u,--update. Command git pull to update." +} + +case $* in +--install|-i) +Pokebotinstall +;; +--encrypt|-e) +Pokebotencrypt +;; +--reset|-r) +Pokebotreset +;; +--update|-u) +Pokebotupdate +;; +--backup|-b) +mkdir $backuppath +cp -f $pokebotpath/configs/config*.json $backuppath/ +cp -f $pokebotpath/web/config/userdata.js $backuppath/ +echo "Backup complete" +;; +--config|-c) +Pokebotconfig +;; +--help|-h) +Pokebothelp +;; +*.json) +filename=$* +cd $pokebotpath +if [ ! -f ./configs/"$filename" ] +then +echo "There's no ./configs/"$filename" file. It's better to use run.sh not this one." +else +Pokebotrun +fi +;; +*) +Pokebothelp +;; +esac +exit 0 From e03f8341bb536dea3bc9753d705ea21e20a31418 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Tue, 9 Aug 2016 09:32:05 -0700 Subject: [PATCH 111/202] Added +x to run.sh --- run.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 run.sh diff --git a/run.sh b/run.sh old mode 100644 new mode 100755 From c8aaf4b82ca2733897a1a32dac203e23ca605a8d Mon Sep 17 00:00:00 2001 From: Jaap Moolenaar Date: Tue, 9 Aug 2016 18:38:17 +0200 Subject: [PATCH 112/202] Added a configuration option "path_startmode" (conflict merge #2489) (#3270) * Upstream update and merge, with path_startmode configuration * Removed logger and fixed base task path * As per request, path_startmode is now path_start_mode * Removed all logging --- CONTRIBUTORS.md | 1 + configs/config.json.path.example | 1 + pokemongo_bot/cell_workers/follow_path.py | 32 ++++++++++++++++++++++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 6e0e44f5fd..57b289ab90 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -58,3 +58,4 @@ * bigkraig * nikhil-pandey * thebigjc + * JaapMoolenaar diff --git a/configs/config.json.path.example b/configs/config.json.path.example index dec7457246..6f7b04c305 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -60,6 +60,7 @@ "type": "FollowPath", "config": { "path_mode": "loop", + "path_start_mode": "first", "path_file": "configs/path.example.json" } } diff --git a/pokemongo_bot/cell_workers/follow_path.py b/pokemongo_bot/cell_workers/follow_path.py index 6e183ed1d7..1532695bd8 100644 --- a/pokemongo_bot/cell_workers/follow_path.py +++ b/pokemongo_bot/cell_workers/follow_path.py @@ -14,13 +14,19 @@ class FollowPath(BaseTask): SUPPORTED_TASK_API_VERSION = 1 def initialize(self): - self.ptr = 0 self._process_config() self.points = self.load_path() + if self.path_start_mode == 'closest': + self.ptr = self.find_closest_point_idx(self.points) + + else: + self.ptr = 0 + def _process_config(self): self.path_file = self.config.get("path_file", None) self.path_mode = self.config.get("path_mode", "linear") + self.path_start_mode = self.config.get("path_start_mode", "first") def load_path(self): if self.path_file is None: @@ -67,6 +73,30 @@ def load_gpx(self): return points + def find_closest_point_idx(self, points): + + return_idx = 0 + min_distance = float("inf"); + for index in range(len(points)): + point = points[index] + botlat = self.bot.api._position_lat + botlng = self.bot.api._position_lng + lat = float(point['lat']) + lng = float(point['lng']) + + dist = distance( + botlat, + botlng, + lat, + lng + ) + + if dist < min_distance: + min_distance = dist + return_idx = index + + return return_idx + def work(self): point = self.points[self.ptr] lat = float(point['lat']) From 80a307ebd9be44948e3b39c7d3c6457c538946c9 Mon Sep 17 00:00:00 2001 From: Eli White Date: Tue, 9 Aug 2016 10:49:09 -0700 Subject: [PATCH 113/202] Adding documentation for how to use and write plugins (#3254) * Adding documentation for how to use and write plugins * Adding a link to the plugins docs in the Readme --- README.md | 3 +-- docs/plugins.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 docs/plugins.md diff --git a/README.md b/README.md index fd99669e74..592ac5a919 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,6 @@ You can count on the community in #help channel. - [Features](#features) - [Wiki](#wiki) - [Credits](#credits) -- [Donation](#donation) - ## Features - [x] GPS Location configuration @@ -56,6 +54,7 @@ All information on [Getting Started](https://github.com/PokemonGoF/PokemonGo-Bot - [Mac] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-mac) - [Windows] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-windows) - [Develop PokemonGo-Bot](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Develop-PokemonGo-Bot) +- [Plugins](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev/wiki/plugins.md) - [Configuration-files](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files) - [Front end web module - Google Maps API] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page)) - [Docker Usage](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ#how-to-run-with-docker) diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 0000000000..d2aaff50c2 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,66 @@ +# PokemonGo-Bot Plugins +Plugins are collections of tasks distributed outside of the main repo. Using plugins lets you use community built tasks that haven't been accepted into the core bot. Some tasks are better suited to not live in the main bot. An example might be a task that reports seen pokemon to a central server. + +## Using Plugins +Plugins are used by adding some new information to your `config.json`. + +In your `config.json`, you can add a new array: + +``` + ... + "plugins": [ + ], + ... +``` + +In this array, you can put a Github URL that contains the revision you want to use. For example: + +``` + ... + "plugins": [ + "TheSavior/test-pgo-plugin#2d54eddde33061be9b329efae0cfb9bd58842655" + ], + ... +``` + +Once that is there, you can add to your `tasks` array the task you want to use from the plugin. Plugins can expose many tasks, check the plugin's documentation for what tasks can be used. + +``` + ... + "tasks": [ + { + "type": "test-pgo-plugin.PrintText" + } + ] + .. +``` + +Then start the bot. It will download the specified plugins and use them when requested in your `tasks` list. + +## Developing Plugins +The plugins array can be given a full path to a folder containing a plugin as well as the github url format. When developing a plugin, use a directory outside the root of the bot and add it to your plugins array. Unlike the github url format, it won't be copied to the bot when it is started up, it will be loaded directly from the specified directory. + +Plugins have access to any of the things that the tasks in the official repo have access to. + +### Example +I recommend looking at this plugin for an example of how to write a plugin: https://github.com/TheSavior/test-pgo-plugin + +### API Versioning +We may at some point need to make a backwards incompatible change to the plugin BaseTask. We will avoid this as much as possible, but in the event that occurs, this is how incompatibilities are detected: + +The `BaseTask` class specifies: + +``` +class BaseTask(object): + TASK_API_VERSION = 1 +``` + +When we need to make a backwards incompatible change, we will increment that number. Plugins have the following: + +``` +class PrintText(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 +``` + +If a user tries to use a plugin that has a `SUPPORTED_TASK_API_VERSION` that does not match the current bot's `TASK_API_VERSION`, an exception will be raised. + From 932fd2bb93bf0a6f8bee68f49917b0ef341ee586 Mon Sep 17 00:00:00 2001 From: Eli White Date: Tue, 9 Aug 2016 10:50:45 -0700 Subject: [PATCH 114/202] Updating link to the plugin docs in the readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 592ac5a919..d372870ca6 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ All information on [Getting Started](https://github.com/PokemonGoF/PokemonGo-Bot - [Mac] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-mac) - [Windows] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-windows) - [Develop PokemonGo-Bot](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Develop-PokemonGo-Bot) -- [Plugins](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev/wiki/plugins.md) +- [Plugins](https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/plugins.md) - [Configuration-files](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files) - [Front end web module - Google Maps API] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page)) - [Docker Usage](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ#how-to-run-with-docker) From 63f777bac0c8cdc75d07f9ef491ca8b900c0148a Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Tue, 9 Aug 2016 11:22:48 -0700 Subject: [PATCH 115/202] Dev merge to master, PR (#3334) * Adding plugin support (#2679) * Adding plugin support * Adding an empty __init__.py * Moving the base task to the project root (#2702) * Moving the base task to the project root * Moving the base class more * Changing the import again * Adding a heartbeat to the analytics (#2709) * Adding a heartbeat to the analytics * Heartbeat every 30 seconds, not every 5 * Don't double track clients * Fix 'local variable 'bot' referenced before assignment' * Providing an error if tasks don't work for the given api (#2732) * Fix for utf8 encoding when catching lured pokemon (#2720) * Fixing lure pokestop encoding * fixing lure encoding * Fix For catchable not being displayed on the web (#2719) * Fix For catchable not being displayed on the web * Update catch_visible_pokemon.py * Added encrypt.so compilation process to Dockerfile (#2695) * OS Detection for encrypt lib (#2768) Fix 32bit check, darwin and linux use the same file Make it a function Check if file exists, if not show error Define file_name first Fix return Check if file exists, if not show error Print info about paths Fix for 32/64bit detection * Fix Typo in unexpected_response_retry (#2531) fixes #2525 #2523 * Revert "changing license from MIT to GPLv3" This reverts commit 69fb64f2bf7c12e28c2bb6d2b636c6af55822448. * When the google analytics domain is blocked the bot crashed. (#2764) With a simple try / except this can be solved. Fix dirty catch all * Fixes #2698 - Prevents "Possibly searching too often" error after re-login. (#2771) * Fixes #2698 - Added api.activate_signature call to prevent issue after re-login. - Also replaced deprecated log call with event_manager emit to prevent exception being thrown. * Modified to use OS detected library path as per PR #2768 * Support loading plugins from .zip files (#2766) * Keep track of how many pokemon released (#2884) * Setting Library path to work with encrypt.so (#2899) Setting LD_LIBRARY_PATH on Dockerfile * :sparkles: Added login and username to available stats (#2494) Added a player_data property in PokemonGoBot to access player data from outside Added unit tests for login and username stats Added tests for call args when updating the window title Added a platform-specific test for window title updating on win32 platform * [dev] small fixes (#2912) * Fixed emit_event typo * Update CONTRIBUTORS.md * Changed initialization location for "bot" We use bot in main exception on 128 * Update pokecli.py * Rename load_path to load_plugin (#2947) * Adding some logic for pulling plugins from github (#2967) * flush after title update (#2977) * correctly re-raise exception to keep backtrace (#2944) * Update MoveToMapPokemon to use events instead of logger. (#2913) * Config/encrypt.so (#2964) * Add config option for libencrypt.so * Correctly set the config value and check for the file in said dir * Fixed mispelling for "formatted" variable (#2984) * Loading plugins from Github (#2992) * Checking github plugin file existence * Loading plugins from github * Fixed #3000 (#3003) Fixed syntax error on "move_to_map_pokemon.py" that makes the client crash when using this feature. * Added MaxPotion inventory count to summary. (#3015) Short Description: The Max Potion count was missing from the inventory summary. Was #2456 * Added cleanup of download and files for encrypt.so after they are no longer needed (#3011) * Fix bot not returning back after telepoting (#3014) * Fix typo: last_long -> last_lon * Whitespace cleanup * Fix bug introduced by #3037: bot not returning back * Fix Dockerfile installation (#3057) * Fix for #3045 (#3055) * Added request to check configuration (#3089) * Fixed Dockerfile - missing \ on command lines (#3096) * Fixed mispelling for "formatted" variable * Docker commands missing trailing \ * Fix for FileIO slowing bot performance.This puts the map writing into a thread and makes sure it only executes once. (#3100) * Change word usage: "fled" to "escaped" (#3118) "fled" is confusing to lot of people and is easily confused with pokemon vanishing. "escaped" is a better term. * Update the example config file (#3120) * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Rename path.example.json to path.json.example * typo: logrmation -> information (#2601) Fix a typo. I assume that it was "information" initially, but became "logrmation" when someone used replace all functionality to replace all infos with logs. But I might be totally wrong at this point, idk. Just didn't like the word and wanted to fix that typo. * Change fled to escaped (#3129) Fix an issue after PR #3118 * When JSON parsing fails, give a rough indication of why (#3137) * When JSON parsing fails, give a rough indication of why * Use the official package instead of SHA1 commit * Handle Github Download Zip Format (#3108) * Checking github plugin file existence * Loading plugins from github * Starting install code for github plugins * Updating GithubPlugin to support extracting folders * Handling github zip formats by extracting to the correct location * Refactor catch worker (#2527) * refactor catch worker * fix * few renames * add to contributors * fix * add missing behavior * fix encounter events * don't make events about ignored pokemon * Added Run-Loop (#3143) * Add files via upload modified run script wich let you run the boot in a loop(if it crashes it restarts) * Integreated Loop into run.sh modified run.sh to loop the script so that even if it crashes it automaticly restarts. * fixing loop in spin fort task (#3165) * Some love for the vim users (#3154) * Updated README with link to desktop version (#3208) * Fix for #3190 (#3197) * MoveToMap: Add minimum balls to run (#3166) * added config to ignore item count for Spin and MoveToFort (#3160) * [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 * Keeping a cache of gym information (#3236) * New Option: "dont_nickname_favorite" (#2496) * New Option: "dont_nickname_favorite" This change (line 19) adds the option, that the user can choose, whether their favorite pokemons should also get a new nickname or not. If a user want this, then he or she has to add the line ("dont_nickname_favorite" = true) after ("nickname_template": " ... ",). * Update nickname_pokemon.py * Update * Put change to line 30 This reduce the reduce the runtime, because favorite pokemon won't be added to the list. * Restart the loop when catching pokemon and there are more to catch (#3242) * fixed NameError: global name 'pokemon_name' is not defined (#3244) resolves ```traceback (most recent call last): File "pokecli.py", line 521, in main() File "pokecli.py", line 95, in main bot.tick() File "/usr/src/app/pokemongo_bot/__init__.py", line 451, in tick if worker.work() == WorkerResult.RUNNING: File "/usr/src/app/pokemongo_bot/cell_workers/evolve_pokemon.py", line 38, in work self._execute_pokemon_evolve(pokemon, cache) File "/usr/src/app/pokemongo_bot/cell_workers/evolve_pokemon.py", line 117, in _execute_pokemon_evolve cache[pokemon.name] = 1 NameError: global name 'pokemon_name' is not defined``` * Stop fetching gym details (#3245) * Checking all forts for lured pokemon (#3163) * Fix flooding of keep_best_release (#3223) * Fix flooding of keep_best_release * Fix flooding of keep_best_release * [Feature] Recycle Threshold (#2465) * Add Threshold Option * Add Threshold Option to Example Configs * Add Name to Contributors * Change config name and message * Remove logger * Add option to run when storage less than something * Change Message * Fix * Error fixes, message improvement * Config Changes and Remove Option * Call heartbeat on step_walker even if speed is higher than distance (#2513) * Return an empty list if no pokemon are available. (#3259) The changes introduced in 4c95259 expose this bug. * Allow UpdateTitleStats to emit events instead of rewriting the console (#3264) * Updating our issue and PR templates to be more helpful (#3262) * Dev (#3277) * * adding enhanced sniping capabilities for move_to_map_pokemon * Adding enhanced sniping capabilities for move_to_map_pokemon * Update pgoapi to a newer version (#3241) This should hopefully fix issues like #3181, #3098, #2874 and potentially more. Needs testing/verification. I am running now, but it does take about an hour to trigger. * Fix unexpected egg incubation retry (#3276) incubator['used'] flag is set but not used in IncubateEggs._apply_incubators * has_next_evolution is a function not a property (#3284) * Powerful setup.sh (#3263) * Rewrite run.sh Very powerful run.sh with lots of function. 1.install(make .so) 2.update 3.config generator 4.config backup 5.run loop make it never down It should run like run.sh *.json or other opinion. See -help. * Update run.sh * Update run.sh OK problem solved * Delete setup.py * Rename run.sh to setup.sh * Create run.sh * Update setup.sh * Update install.sh * Update setup.sh * Update run.sh * Update setup.sh Some small fix. * Added +x to run.sh * Added a configuration option "path_startmode" (conflict merge #2489) (#3270) * Upstream update and merge, with path_startmode configuration * Removed logger and fixed base task path * As per request, path_startmode is now path_start_mode * Removed all logging * Adding documentation for how to use and write plugins (#3254) * Adding documentation for how to use and write plugins * Adding a link to the plugins docs in the Readme * Updating link to the plugin docs in the readme --- .github/ISSUE_TEMPLATE.md | 12 +- .github/PULL_REQUEST_TEMPLATE.md | 18 +- .gitignore | 3 + CONTRIBUTORS.md | 5 + README.md | 5 +- configs/config.json.cluster.example | 1 + configs/config.json.example | 1 + configs/config.json.map.example | 6 +- configs/config.json.path.example | 2 + configs/config.json.pokemon.example | 1 + docs/plugins.md | 66 ++ install.sh | 79 +- pokemongo_bot/__init__.py | 36 +- .../cell_workers/catch_lured_pokemon.py | 49 +- .../cell_workers/catch_visible_pokemon.py | 28 +- pokemongo_bot/cell_workers/evolve_pokemon.py | 87 +- pokemongo_bot/cell_workers/follow_path.py | 32 +- pokemongo_bot/cell_workers/incubate_eggs.py | 2 + pokemongo_bot/cell_workers/move_to_fort.py | 9 +- .../cell_workers/move_to_map_pokemon.py | 9 +- .../cell_workers/nickname_pokemon.py | 2 +- .../cell_workers/pokemon_catch_worker.py | 823 ++++++++---------- pokemongo_bot/cell_workers/recycle_items.py | 23 +- pokemongo_bot/cell_workers/spin_fort.py | 13 +- .../cell_workers/transfer_pokemon.py | 21 +- .../cell_workers/update_title_stats.py | 11 + pokemongo_bot/inventory.py | 251 ++++++ pokemongo_bot/step_walker.py | 1 + pokemongo_bot/worker_result.py | 1 + requirements.txt | 2 +- run.sh | 29 +- setup.py | 13 - setup.sh | 135 +++ 33 files changed, 1139 insertions(+), 637 deletions(-) create mode 100644 docs/plugins.md create mode 100644 pokemongo_bot/inventory.py delete mode 100644 setup.py create mode 100755 setup.sh diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index a1d7168e75..e5a3f1f317 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,7 @@ -Please check configuration at http://jsonlint.com/ before posting an issue. +## Stop! Before you create this issue (you can delete this section when opening the issue): +1. Have you validated that your config.json is valid JSON? Use http://jsonlint.com/ to check. +2. Have you searched to see if there are other issues for the same issue? If so, comment on that issue instead. +3. Are you running `master`? We work on the `dev` branch and then add that functionality to `master`. Your issue may be fixed on `dev` and there is no need for this issue, just wait and it will eventually be merged to `master`. ### Expected Behavior @@ -6,10 +9,15 @@ Please check configuration at http://jsonlint.com/ before posting an issue. ### Actual Behavior +### Your config.json (remove your credentials and any other private info) +``` +your config here +``` + ### Steps to Reproduce ### Other Information -OS: +OS: Git Commit: (run 'git log -n 1 --pretty=format:"%H"' and paste it here) Python Version: (run 'python -V' and paste it here) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5178d928b5..a43df95477 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,15 @@ -Short Description: +# Please Note (you may remove this section before opening your PR): +We receive lots of PRs and it is hard to give proper review to PRs. Please make it easy on us by following these guidelines: -Fixes: -- -- -- +1. We do not accept changes to `master`. Please make sure your pull request is aimed at `dev`. +2. If you changed a bunch of files (that aren't config files) or multiple workers to implement your feature, it probably won't get proper attention. Please split it up into multiple, smaller, more focused, and iterative PRs if you can. +3. If you are adding a config value to something, make sure you update the appropriate `config.json` example files. + + +## Short Description: + +## Fixes (provide links to github issues if you can): +- +- +- diff --git a/.gitignore b/.gitignore index 70ab34b250..4721ce0253 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,6 @@ include/ # Pip check file pip-selfcheck.json + +# Some love for the vim users +.*.sw* diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 6dceaf0918..57b289ab90 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -51,6 +51,11 @@ * matheussampaio * Abraxas000 * lucasfevi + * pokepal * Moonlight-Angel * mjmadsen * nikofil + * bigkraig + * nikhil-pandey + * thebigjc + * JaapMoolenaar diff --git a/README.md b/README.md index 171ee4f355..d372870ca6 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ PokemonGo bot is a project created by the [PokemonGoF](https://github.com/PokemonGoF) team. The project is currently setup in two main branches. `dev` and `master`. +## Help Needed on [Desktop Version](https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) + ## Please submit PR to [Dev branch](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) @@ -12,8 +14,6 @@ You can count on the community in #help channel. - [Features](#features) - [Wiki](#wiki) - [Credits](#credits) -- [Donation](#donation) - ## Features - [x] GPS Location configuration @@ -54,6 +54,7 @@ All information on [Getting Started](https://github.com/PokemonGoF/PokemonGo-Bot - [Mac] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-mac) - [Windows] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-windows) - [Develop PokemonGo-Bot](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Develop-PokemonGo-Bot) +- [Plugins](https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/plugins.md) - [Configuration-files](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files) - [Front end web module - Google Maps API] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page)) - [Docker Usage](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ#how-to-run-with-docker) diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index b32eb4f668..e4597519f6 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/configs/config.json.example b/configs/config.json.example index ec46c15eb9..d2e8d4f064 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/configs/config.json.map.example b/configs/config.json.map.example index 1079c999f9..bb6878f5ac 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -33,6 +33,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, @@ -58,8 +59,11 @@ "address": "http://localhost:5000", "max_distance": 500, "min_time": 60, + "min_ball": 50, "prioritize_vips": true, - "snipe": false, + "snipe": true, + "snipe_high_prio_only": true, + "snipe_high_prio_threshold": 400, "update_map": true, "mode": "priority", "catch": { diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 94a9fdba07..6f7b04c305 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, @@ -59,6 +60,7 @@ "type": "FollowPath", "config": { "path_mode": "loop", + "path_start_mode": "first", "path_file": "configs/path.example.json" } } diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 1d428a6ae7..e7ba38dc37 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 0000000000..d2aaff50c2 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,66 @@ +# PokemonGo-Bot Plugins +Plugins are collections of tasks distributed outside of the main repo. Using plugins lets you use community built tasks that haven't been accepted into the core bot. Some tasks are better suited to not live in the main bot. An example might be a task that reports seen pokemon to a central server. + +## Using Plugins +Plugins are used by adding some new information to your `config.json`. + +In your `config.json`, you can add a new array: + +``` + ... + "plugins": [ + ], + ... +``` + +In this array, you can put a Github URL that contains the revision you want to use. For example: + +``` + ... + "plugins": [ + "TheSavior/test-pgo-plugin#2d54eddde33061be9b329efae0cfb9bd58842655" + ], + ... +``` + +Once that is there, you can add to your `tasks` array the task you want to use from the plugin. Plugins can expose many tasks, check the plugin's documentation for what tasks can be used. + +``` + ... + "tasks": [ + { + "type": "test-pgo-plugin.PrintText" + } + ] + .. +``` + +Then start the bot. It will download the specified plugins and use them when requested in your `tasks` list. + +## Developing Plugins +The plugins array can be given a full path to a folder containing a plugin as well as the github url format. When developing a plugin, use a directory outside the root of the bot and add it to your plugins array. Unlike the github url format, it won't be copied to the bot when it is started up, it will be loaded directly from the specified directory. + +Plugins have access to any of the things that the tasks in the official repo have access to. + +### Example +I recommend looking at this plugin for an example of how to write a plugin: https://github.com/TheSavior/test-pgo-plugin + +### API Versioning +We may at some point need to make a backwards incompatible change to the plugin BaseTask. We will avoid this as much as possible, but in the event that occurs, this is how incompatibilities are detected: + +The `BaseTask` class specifies: + +``` +class BaseTask(object): + TASK_API_VERSION = 1 +``` + +When we need to make a backwards incompatible change, we will increment that number. Plugins have the following: + +``` +class PrintText(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 +``` + +If a user tries to use a plugin that has a `SUPPORTED_TASK_API_VERSION` that does not match the current bot's `TASK_API_VERSION`, an exception will be raised. + diff --git a/install.sh b/install.sh index c11992bb0c..32cc1e1124 100755 --- a/install.sh +++ b/install.sh @@ -1,19 +1,62 @@ #!/usr/bin/env bash - -# Setup Python virtualenv -echo "Setting up Python virtualenv..." -eval "virtualenv ." -eval "source bin/activate" -echo "Python virtualenv setup successfully." - -# Install pip requirements -echo "Installing pip requirements..." -eval "pip install -r requirements.txt" -echo "Installed pip requirements." -echo "Installing and updating git submodules..." - -# Install git submodules -eval "cd ./web && git submodule init && cd .." -eval "git submodule update" -echo "Done." -echo "Please create and setup configs/config.json. Then, run 'python pokecli.py --config ./configs/config.json' or './run.sh' on Mac/Linux" \ No newline at end of file +pokebotpath=$(pwd) +cd $pokebotpath +if [ -f /etc/debian_version ] +then +echo "You are on Debian/Ubuntu" +sudo apt-get update +sudo apt-get -y install python python-pip python-dev build-essential git python-protobuf virtualenv +elif [ -f /etc/redhat-release ] +then +echo "You are on CentOS/RedHat" +sudo yum -y install epel-release +sudo yum -y install python-pip +elif [ "$(uname -s)" == "Darwin" ] +then +echo "You are on Mac os" +sudo brew update +sudo brew install --devel protobuf +else +echo "Nothing happend." +fi +pip install virtualenv +cd $pokebotpath +git pull +git submodule init +git submodule foreach git pull origin master +virtualenv . +source bin/activate +pip install -r requirements.txt +echo "Start to make encrypt.so" +wget http://pgoapi.com/pgoencrypt.tar.gz +tar -xf pgoencrypt.tar.gz +cd pgoencrypt/src/ +make +mv libencrypt.so $pokebotpath/encrypt.so +cd ../.. +rm -rf pgoencrypt.tar.gz +rm -rf pgoencrypt +echo "Install complete." +cd $pokebotpath +read -p "1.google 2.ptc +" auth +read -p "Input username +" username +read -p "Input password +" -s password +read -p " +Input location +" location +read -p "Input gmapkey +" gmapkey +cp configs/config.json.example configs/config.json +if [ "$auth" = "2" ] +then +sed -i "s/google/ptc/g" configs/config.json +fi +sed -i "s/YOUR_USERNAME/$username/g" configs/config.json +sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json +sed -i "s/SOME_LOCATION/$location/g" configs/config.json +sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json +echo "Edit configs/config.json to modify any other config. Use run.sh to run." +exit 0 diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 8e9a28ce3a..dd4de22551 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): @@ -232,10 +235,12 @@ def _register_events(self): 'iv_display', ) ) + self.event_manager.register_event('no_pokeballs') self.event_manager.register_event( 'pokemon_catch_rate', parameters=( 'catch_rate', + 'ball_name', 'berry_name', 'berry_count' ) @@ -244,25 +249,28 @@ def _register_events(self): 'threw_berry', parameters=( 'berry_name', + 'ball_name', 'new_catch_rate' ) ) self.event_manager.register_event( 'threw_pokeball', parameters=( - 'pokeball', + 'ball_name', 'success_percentage', 'count_left' ) ) self.event_manager.register_event( - 'pokemon_escaped', + 'pokemon_capture_failed', parameters=('pokemon',) ) self.event_manager.register_event( 'pokemon_vanished', parameters=('pokemon',) ) + self.event_manager.register_event('pokemon_not_in_range') + self.event_manager.register_event('pokemon_inventory_full') self.event_manager.register_event( 'pokemon_caught', parameters=( @@ -281,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( @@ -341,6 +349,10 @@ def _register_events(self): 'amount', 'item', 'maximum' ) ) + self.event_manager.register_event( + 'item_discard_skipped', + parameters=('space',) + ) self.event_manager.register_event( 'item_discard_fail', parameters=('item',) @@ -486,22 +498,6 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): location = self.position[0:2] cells = self.find_close_cells(*location) - # insert detail info about gym to fort - for cell in cells: - if 'forts' in cell: - for fort in cell['forts']: - if fort.get('type') != 1: - 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') - ) - fort['gym_details'] = response_gym_details.get( - 'responses', {} - ).get('GET_GYM_DETAILS', None) - user_data_cells = "data/cells-%s.json" % self.config.username with open(user_data_cells, 'w') as outfile: json.dump(cells, outfile) @@ -777,6 +773,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/catch_lured_pokemon.py b/pokemongo_bot/cell_workers/catch_lured_pokemon.py index 10a046dce9..afddeb53d5 100644 --- a/pokemongo_bot/cell_workers/catch_lured_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_lured_pokemon.py @@ -1,9 +1,11 @@ # -*- 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.worker_result import WorkerResult +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): @@ -11,24 +13,42 @@ class CatchLuredPokemon(BaseTask): def work(self): lured_pokemon = self.get_lured_pokemon() - if lured_pokemon: - self.catch_pokemon(lured_pokemon) + if len(lured_pokemon) > 0: + self.catch_pokemon(lured_pokemon[0]) + + if len(lured_pokemon) > 1: + return WorkerResult.RUNNING + + return WorkerResult.SUCCESS def get_lured_pokemon(self): + forts_in_range = [] + pokemon_to_catch = [] forts = self.bot.get_forts(order_by_distance=True) if len(forts) == 0: - return False + return [] - 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) - if encounter_id: + 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'] + result = { 'encounter_id': encounter_id, 'fort_id': fort['id'], @@ -36,15 +56,14 @@ def get_lured_pokemon(self): '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) diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py index 1bfed225df..654c2467b3 100644 --- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -3,13 +3,24 @@ from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker from utils import distance +from pokemongo_bot.worker_result import WorkerResult class CatchVisiblePokemon(BaseTask): SUPPORTED_TASK_API_VERSION = 1 def work(self): - if 'catchable_pokemons' in self.bot.cell and len(self.bot.cell['catchable_pokemons']) > 0: + num_catchable_pokemon = 0 + if 'catchable_pokemons' in self.bot.cell: + num_catchable_pokemon = len(self.bot.cell['catchable_pokemons']) + + num_wild_pokemon = 0 + if 'wild_pokemons' in self.bot.cell: + num_wild_pokemon = len(self.bot.cell['wild_pokemons']) + + num_available_pokemon = num_catchable_pokemon + num_wild_pokemon + + if num_catchable_pokemon > 0: # Sort all by distance from current pos- eventually this should # build graph & A* it self.bot.cell['catchable_pokemons'].sort( @@ -33,15 +44,24 @@ def work(self): } ) - return self.catch_pokemon(self.bot.cell['catchable_pokemons'].pop(0)) + self.catch_pokemon(self.bot.cell['catchable_pokemons'].pop(0)) + if num_catchable_pokemon > 1: + return WorkerResult.RUNNING + else: + return WorkerResult.SUCCESS - if 'wild_pokemons' in self.bot.cell and len(self.bot.cell['wild_pokemons']) > 0: + if num_available_pokemon > 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)) + self.catch_pokemon(self.bot.cell['wild_pokemons'].pop(0)) + + if num_catchable_pokemon > 1: + return WorkerResult.RUNNING + else: + return WorkerResult.SUCCESS def catch_pokemon(self, pokemon): worker = PokemonCatchWorker(pokemon, self.bot) diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index c3903a685d..4cc451115b 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,86 +76,45 @@ 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: # cache pokemons we can't evolve. Less server calls - cache[pokemon_name] = 1 + cache[pokemon.name] = 1 sleep(0.7) return False diff --git a/pokemongo_bot/cell_workers/follow_path.py b/pokemongo_bot/cell_workers/follow_path.py index 6e183ed1d7..1532695bd8 100644 --- a/pokemongo_bot/cell_workers/follow_path.py +++ b/pokemongo_bot/cell_workers/follow_path.py @@ -14,13 +14,19 @@ class FollowPath(BaseTask): SUPPORTED_TASK_API_VERSION = 1 def initialize(self): - self.ptr = 0 self._process_config() self.points = self.load_path() + if self.path_start_mode == 'closest': + self.ptr = self.find_closest_point_idx(self.points) + + else: + self.ptr = 0 + def _process_config(self): self.path_file = self.config.get("path_file", None) self.path_mode = self.config.get("path_mode", "linear") + self.path_start_mode = self.config.get("path_start_mode", "first") def load_path(self): if self.path_file is None: @@ -67,6 +73,30 @@ def load_gpx(self): return points + def find_closest_point_idx(self, points): + + return_idx = 0 + min_distance = float("inf"); + for index in range(len(points)): + point = points[index] + botlat = self.bot.api._position_lat + botlng = self.bot.api._position_lng + lat = float(point['lat']) + lng = float(point['lng']) + + dist = distance( + botlat, + botlng, + lat, + lng + ) + + if dist < min_distance: + min_distance = dist + return_idx = index + + return return_idx + def work(self): point = self.points[self.ptr] lat = float(point['lat']) diff --git a/pokemongo_bot/cell_workers/incubate_eggs.py b/pokemongo_bot/cell_workers/incubate_eggs.py index 5761090ea5..805129435b 100644 --- a/pokemongo_bot/cell_workers/incubate_eggs.py +++ b/pokemongo_bot/cell_workers/incubate_eggs.py @@ -49,6 +49,8 @@ def work(self): def _apply_incubators(self): for incubator in self.ready_incubators: + if incubator.get('used', False): + continue for egg in self.eggs: if egg["used"] or egg["km"] == -1: continue diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index e4b4187d20..7dcd0977b1 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -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) diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 9ab6bbb7a2..2cfd45d14b 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -82,6 +82,7 @@ def initialize(self): self.pokemon_data = self.bot.pokemon_list self.unit = self.bot.config.distance_unit self.caught = [] + self.min_ball = self.config.get('min_ball', 1) data_file = 'data/map-caught-{}.json'.format(self.bot.config.username) if os.path.isfile(data_file): @@ -250,11 +251,15 @@ def work(self): pokemon = pokemon_list[0] # if we only have ultraballs and the target is not a vip don't snipe/walk - if (pokeballs + superballs) < 1 and not pokemon['is_vip']: + if (pokeballs + superballs) < self.min_ball and not pokemon['is_vip']: return WorkerResult.SUCCESS if self.config['snipe']: - return self.snipe(pokemon) + if self.config['snipe_high_prio_only']: + if self.config['snipe_high_prio_threshold'] < pokemon['priority'] or pokemon['is_vip']: + self.snipe(pokemon) + else: + return self.snipe(pokemon) step_walker = self._move_to(pokemon) if not step_walker.step(): diff --git a/pokemongo_bot/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py index cda206ad20..e521cadfba 100644 --- a/pokemongo_bot/cell_workers/nickname_pokemon.py +++ b/pokemongo_bot/cell_workers/nickname_pokemon.py @@ -27,7 +27,7 @@ def _get_inventory_pokemon(self,inventory_dict): except KeyError: pass else: - if not pokemon.get('is_egg',False): + if not pokemon.get('is_egg',False) and not (pokemon.get('favorite', 0) == 1 and self.config.get('dont_nickname_favorite',False)): pokemon_data.append(pokemon) return pokemon_data diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index d5118b391b..d551a68632 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -1,13 +1,50 @@ # -*- coding: utf-8 -*- import time -from pokemongo_bot.human_behaviour import (normalized_reticle_size, sleep, - spin_modifier) +from pokemongo_bot import inventory from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.human_behaviour import normalized_reticle_size, sleep, spin_modifier +from pokemongo_bot.worker_result import WorkerResult + +CATCH_STATUS_SUCCESS = 1 +CATCH_STATUS_FAILED = 2 +CATCH_STATUS_VANISHED = 3 + +ENCOUNTER_STATUS_SUCCESS = 1 +ENCOUNTER_STATUS_NOT_IN_RANGE = 5 +ENCOUNTER_STATUS_POKEMON_INVENTORY_FULL = 7 + +ITEM_POKEBALL = 1 +ITEM_GREATBALL = 2 +ITEM_ULTRABALL = 3 +ITEM_RAZZBERRY = 701 + +LOGIC_TO_FUNCTION = { + 'or': lambda x, y: x or y, + 'and': lambda x, y: x and y +} + + +class Pokemon(object): + + def __init__(self, pokemon_list, pokemon_data): + 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) + self.stamina = pokemon_data.get('individual_stamina', 0) + + @property + def iv(self): + return round((self.attack + self.defense + self.stamina) / 45.0, 2) + + @property + def iv_display(self): + return '{}/{}/{}'.format(self.attack, self.defense, self.stamina) + class PokemonCatchWorker(BaseTask): - BAG_FULL = 'bag_full' - NO_POKEBALLS = 'no_pokeballs' def __init__(self, pokemon, bot): self.pokemon = pokemon @@ -22,444 +59,63 @@ def __init__(self, pokemon, bot): self.response_key = '' self.response_status_key = '' + ############################################################################ + # public methods + ############################################################################ + def work(self, response_dict=None): - encounter_id = self.pokemon['encounter_id'] + response_dict = response_dict or self.create_encounter_api_call() + # validate response if not response_dict: - response_dict = self.create_encounter_api_call() - - if response_dict and 'responses' in response_dict: - if self.response_key in response_dict['responses']: - if self.response_status_key in response_dict['responses'][self.response_key]: - if response_dict['responses'][self.response_key][self.response_status_key] is 1: - cp = 0 - if 'wild_pokemon' in response_dict['responses'][self.response_key] or 'pokemon_data' in \ - response_dict['responses'][self.response_key]: - if self.response_key == 'ENCOUNTER': - pokemon = response_dict['responses'][self.response_key]['wild_pokemon'] - else: - pokemon = response_dict['responses'][self.response_key] - - catch_rate = response_dict['responses'][self.response_key]['capture_probability'][ - 'capture_probability'] # 0 = pokeballs, 1 great balls, 3 ultra balls - - if 'pokemon_data' in pokemon and 'cp' in pokemon['pokemon_data']: - pokemon_data = pokemon['pokemon_data'] - cp = pokemon_data['cp'] - - individual_attack = pokemon_data.get("individual_attack", 0) - individual_stamina = pokemon_data.get("individual_stamina", 0) - individual_defense = pokemon_data.get("individual_defense", 0) - - iv_display = '{}/{}/{}'.format( - individual_attack, - individual_defense, - individual_stamina - ) - - pokemon_potential = self.pokemon_potential(pokemon_data) - pokemon_num = int(pokemon_data['pokemon_id']) - 1 - pokemon_name = self.pokemon_list[int(pokemon_num)]['Name'] - - msg = 'A wild {pokemon} appeared! [CP {cp}] [Potential {iv}] [S/A/D {iv_display}]' - self.emit_event( - 'pokemon_appeared', - formatted=msg, - data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': pokemon_potential, - 'iv_display': iv_display, - } - ) - - pokemon_data['name'] = pokemon_name - # Simulate app - sleep(3) - - if not self.should_capture_pokemon(pokemon_name, cp, pokemon_potential, response_dict): - return False - - flag_VIP = False - # @TODO, use the best ball in stock to catch VIP (Very Important Pokemon: Configurable) - if self.check_vip_pokemon(pokemon_name, cp, pokemon_potential): - self.emit_event( - 'vip_pokemon', - formatted='This is a VIP pokemon. Catch!!!' - ) - flag_VIP=True - - items_stock = self.bot.current_inventory() - berry_id = 701 # @ TODO: use better berries if possible - berries_count = self.bot.item_inventory_count(berry_id) - while True: - # pick the most simple ball from stock - pokeball = 1 # start from 1 - PokeBalls - berry_used = False - - if flag_VIP: - if(berries_count>0 and catch_rate[pokeball-1] < 0.9): - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - self.emit_event( - 'pokemon_catch_rate', - level='debug', - formatted="Catch rate of {catch_rate} is low. Maybe will throw {berry_name} ({berry_count} left)", - data={ - 'catch_rate': success_percentage, - 'berry_name': self.item_list[str(berry_id)], - 'berry_count': berries_count - } - ) - # Out of all pokeballs! Let's don't waste berry. - if items_stock[1] == 0 and items_stock[2] == 0 and items_stock[3] == 0: - break - - # Use the berry to catch - 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']: - catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - berries_count = berries_count -1 - berry_used = True - self.emit_event( - 'threw_berry', - formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", - data={ - "berry_name": self.item_list[str(berry_id)], - "new_catch_rate": success_percentage - } - ) - else: - if response_dict['status_code'] is 1: - self.emit_event( - 'softban', - level='warning', - formatted='Failed to use berry. You may be softbanned.' - ) - else: - self.emit_event( - 'threw_berry_failed', - formatted='Unknown response when throwing berry: {status_code}.', - data={ - 'status_code': response_dict['status_code'] - } - ) - - #use the best ball to catch - current_type = pokeball - #debug use normal ball - while current_type < 3: - current_type += 1 - if catch_rate[pokeball-1] < 0.9 and items_stock[current_type] > 0: - # if current ball chance to catch is under 90%, and player has better ball - then use it - pokeball = current_type # use better ball - else: - # If we have a lot of berries (than the great ball), we prefer use a berry first! - if catch_rate[pokeball-1] < 0.42 and items_stock[pokeball+1]+30 < berries_count: - # If it's not the VIP type, we don't want to waste our ultra ball if no balls left. - if items_stock[1] == 0 and items_stock[2] == 0: - break - - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - self.emit_event( - 'pokemon_catch_rate', - level='debug', - formatted="Catch rate of {catch_rate} is low. Maybe will throw {berry_name} ({berry_count} left)", - data={ - 'catch_rate': success_percentage, - 'berry_name': self.item_list[str(berry_id)], - 'berry_count': berries_count-1 - } - ) - 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']: - catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - berries_count = berries_count -1 - berry_used = True - self.emit_event( - 'threw_berry', - formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", - data={ - "berry_name": self.item_list[str(berry_id)], - "new_catch_rate": success_percentage - } - ) - else: - if response_dict['status_code'] is 1: - self.emit_event( - 'softban', - level='warning', - formatted='Failed to use berry. You may be softbanned.' - ) - else: - self.emit_event( - 'threw_berry_failed', - formatted='Unknown response when throwing berry: {status_code}.', - data={ - 'status_code': response_dict['status_code'] - } - ) - - else: - #We don't have many berry to waste, pick a good ball first. Save some berry for future VIP pokemon - current_type = pokeball - while current_type < 2: - current_type += 1 - if catch_rate[pokeball-1] < 0.35 and items_stock[current_type] > 0: - # if current ball chance to catch is under 35%, and player has better ball - then use it - pokeball = current_type # use better ball - - #if the rate is still low and we didn't throw a berry before use berry - if catch_rate[pokeball-1] < 0.35 and berries_count > 0 and berry_used == False: - # If it's not the VIP type, we don't want to waste our ultra ball if no balls left. - if items_stock[1] == 0 and items_stock[2] == 0: - break - - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - self.emit_event( - 'pokemon_catch_rate', - level='debug', - formatted="Catch rate of {catch_rate} is low. Throwing {berry_name} ({berry_count} left)", - data={ - 'catch_rate': success_percentage, - 'berry_name': self.item_list[str(berry_id)], - 'berry_count': berries_count-1 - } - ) - 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']: - catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - berries_count = berries_count -1 - berry_used = True - self.emit_event( - 'threw_berry', - formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", - data={ - "berry_name": self.item_list[str(berry_id)], - "new_catch_rate": success_percentage - } - ) - else: - if response_dict['status_code'] is 1: - self.emit_event( - 'softban', - level='warning', - formatted='Failed to use berry. You may be softbanned.' - ) - else: - self.emit_event( - 'threw_berry_failed', - formatted='Unknown response when throwing berry: {status_code}.', - data={ - 'status_code': response_dict['status_code'] - } - ) - - # Re-check if berry is used, find a ball for a good capture rate - current_type=pokeball - while current_type < 2: - current_type += 1 - if catch_rate[pokeball-1] < 0.35 and items_stock[current_type] > 0: - pokeball = current_type # use better ball - - # This is to avoid rare case that a berry has ben throwed <0.42 - # and still picking normal pokeball (out of stock) -> error - if items_stock[1] == 0 and items_stock[2] > 0: - pokeball = 2 - - # Add this logic to avoid Pokeball = 0, Great Ball = 0, Ultra Ball = X - # And this logic saves Ultra Balls if it's a weak trash pokemon - if catch_rate[pokeball-1]<0.30 and items_stock[3]>0: - pokeball = 3 - - items_stock[pokeball] -= 1 - success_percentage = '{0:.2f}'.format(catch_rate[pokeball - 1] * 100) - self.emit_event( - 'threw_pokeball', - formatted='Used {pokeball}, with chance {success_percentage} ({count_left} left)', - data={ - 'pokeball': self.item_list[str(pokeball)], - 'success_percentage': success_percentage, - 'count_left': 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) - - response_dict = 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 - ) - - if response_dict and \ - 'responses' in response_dict and \ - 'CATCH_POKEMON' in response_dict['responses'] and \ - 'status' in response_dict['responses']['CATCH_POKEMON']: - status = response_dict['responses'][ - 'CATCH_POKEMON']['status'] - if status is 2: - self.emit_event( - 'pokemon_escaped', - formatted="{pokemon} escaped.", - data={'pokemon': pokemon_name} - ) - sleep(2) - continue - if status is 3: - self.emit_event( - 'pokemon_vanished', - formatted="{pokemon} vanished!", - data={'pokemon': pokemon_name} - ) - if success_percentage == 100: - self.softban = True - if status is 1: - self.bot.metrics.captured_pokemon(pokemon_name, cp, iv_display, pokemon_potential) - - self.emit_event( - 'pokemon_caught', - formatted='Captured {pokemon}! [CP {cp}] [Potential {iv}] [{iv_display}] [+{exp} exp]', - data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': pokemon_potential, - 'iv_display': iv_display, - 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) - } - ) - self.bot.softban = False - - if (self.config.evolve_captured - and (self.config.evolve_captured[0] == 'all' - or pokemon_name in self.config.evolve_captured)): - id_list2 = self.count_pokemon_inventory() - # No need to capture this even for metrics, player stats includes it. - pokemon_to_transfer = list(set(id_list2) - set(id_list1)) - - # TODO dont throw RuntimeError, do something better - if len(pokemon_to_transfer) == 0: - raise RuntimeError( - 'Trying to evolve 0 pokemons!') - response_dict = self.api.evolve_pokemon(pokemon_id=pokemon_to_transfer[0]) - status = response_dict['responses']['EVOLVE_POKEMON']['result'] - if status == 1: - self.emit_event( - 'pokemon_evolved', - formatted="{pokemon} evolved!", - data={'pokemon': pokemon_name} - ) - else: - self.emit_event( - 'pokemon_evolve_fail', - formatted="Failed to evolve {pokemon}!", - data={'pokemon': pokemon_name} - ) - break - time.sleep(5) - - def count_pokemon_inventory(self): - # don't use cached bot.get_inventory() here - # because we need to have actual information in capture logic - response_dict = self.api.get_inventory() - - id_list = [] - callback = lambda pokemon: id_list.append(pokemon['id']) - self._foreach_pokemon_in_inventory(response_dict, callback) - return id_list - - def _foreach_pokemon_in_inventory(self, response_dict, callback): + return WorkerResult.ERROR try: - reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) + responses = response_dict['responses'] + response = responses[self.response_key] + if response[self.response_status_key] != ENCOUNTER_STATUS_SUCCESS: + if response[self.response_status_key] == ENCOUNTER_STATUS_NOT_IN_RANGE: + self.emit_event('pokemon_not_in_range', formatted='Pokemon went out of range!') + elif response[self.response_status_key] == ENCOUNTER_STATUS_POKEMON_INVENTORY_FULL: + self.emit_event('pokemon_inventory_full', formatted='Your Pokemon inventory is full! Could not catch!') + return WorkerResult.ERROR except KeyError: - pass - else: - for item in response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data"], item) - except KeyError: - pass - else: - pokemon = item['inventory_item_data']['pokemon_data'] - if not pokemon.get('is_egg', False): - callback(pokemon) - - def pokemon_potential(self, pokemon_data): - total_iv = 0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - - for individual_stat in iv_stats: - try: - total_iv += pokemon_data[individual_stat] - except: - pokemon_data[individual_stat] = 0 - continue - - return round((total_iv / 45.0), 2) - - def should_capture_pokemon(self, pokemon_name, cp, iv, response_dict): - catch_config = self._get_catch_config_for(pokemon_name) - cp_iv_logic = catch_config.get('logic') - if not cp_iv_logic: - cp_iv_logic = self._get_catch_config_for('any').get('logic', 'and') - - catch_results = { - 'cp': False, - 'iv': False, - } - - if catch_config.get('never_catch', False): - return False - - if catch_config.get('always_catch', False): - return True - - catch_cp = catch_config.get('catch_above_cp', 0) - if cp > catch_cp: - catch_results['cp'] = True - - catch_iv = catch_config.get('catch_above_iv', 0) - if iv > catch_iv: - catch_results['iv'] = True - - logic_to_function = { - 'or': lambda x, y: x or y, - 'and': lambda x, y: x and y - } - - return logic_to_function[cp_iv_logic](*catch_results.values()) + return WorkerResult.ERROR + + # get pokemon data + pokemon_data = response['wild_pokemon']['pokemon_data'] if 'wild_pokemon' in response else response['pokemon_data'] + pokemon = Pokemon(self.pokemon_list, pokemon_data) + + # skip ignored pokemon + if not self._should_catch_pokemon(pokemon): + return WorkerResult.SUCCESS + + # log encounter + self.emit_event( + 'pokemon_appeared', + formatted='A wild {pokemon} appeared! [CP {cp}] [Potential {iv}] [A/D/S {iv_display}]', + data={ + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv, + 'iv_display': pokemon.iv_display, + } + ) + + # simulate app + sleep(3) + + # check for VIP pokemon + is_vip = self._is_vip_pokemon(pokemon) + if is_vip: + self.emit_event('vip_pokemon', formatted='This is a VIP pokemon. Catch!!!') + + # catch that pokemon! + encounter_id = self.pokemon['encounter_id'] + catch_rate_by_ball = [0] + response['capture_probability']['capture_probability'] # offset so item ids match indces + self._do_catch(pokemon, encounter_id, catch_rate_by_ball, is_vip=is_vip) - def _get_catch_config_for(self, pokemon): - catch_config = self.config.catch.get(pokemon) - if not catch_config: - catch_config = self.config.catch.get('any') - return catch_config + # simulate app + time.sleep(5) def create_encounter_api_call(self): encounter_id = self.pokemon['encounter_id'] @@ -491,29 +147,288 @@ def create_encounter_api_call(self): ) return request.call() - def check_vip_pokemon(self,pokemon, cp, iv): + ############################################################################ + # helpers + ############################################################################ + + def _pokemon_matches_config(self, config, pokemon, default_logic='and'): + pokemon_config = config.get(pokemon.name, config.get('any')) + + if not pokemon_config: + return False - vip_name = self.config.vips.get(pokemon) - if vip_name == {}: - return True - else: - catch_config = self.config.vips.get("any") - if not catch_config: - return False - cp_iv_logic = catch_config.get('logic', 'or') catch_results = { 'cp': False, 'iv': False, } - catch_cp = catch_config.get('catch_above_cp', 0) - if cp > catch_cp: + if pokemon_config.get('never_catch', False): + return False + + if pokemon_config.get('always_catch', False): + return True + + catch_cp = pokemon_config.get('catch_above_cp', 0) + if pokemon.cp > catch_cp: catch_results['cp'] = True - catch_iv = catch_config.get('catch_above_iv', 0) - if iv > catch_iv: + + catch_iv = pokemon_config.get('catch_above_iv', 0) + if pokemon.iv > catch_iv: catch_results['iv'] = True - logic_to_function = { - 'or': lambda x, y: x or y, - 'and': lambda x, y: x and y - } - return logic_to_function[cp_iv_logic](*catch_results.values()) + + return LOGIC_TO_FUNCTION[pokemon_config.get('logic', default_logic)](*catch_results.values()) + + def _should_catch_pokemon(self, pokemon): + return self._pokemon_matches_config(self.config.catch, pokemon) + + def _is_vip_pokemon(self, pokemon): + # having just a name present in the list makes them vip + if self.config.vips.get(pokemon.name) == {}: + return True + return self._pokemon_matches_config(self.config.vips, pokemon, default_logic='or') + + def _get_current_pokemon_ids(self): + # don't use cached bot.get_inventory() here because we need to have actual information in capture logic + response_dict = self.api.get_inventory() + + try: + inventory_items = response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] + except KeyError: + return [] # no items + + id_list = [] + for item in inventory_items: + try: + pokemon = item['inventory_item_data']['pokemon_data'] + except KeyError: + continue + + # ignore eggs + if pokemon.get('is_egg'): + continue + + id_list.append(pokemon['id']) + + return id_list + + def _pct(self, rate_by_ball): + return '{0:.2f}'.format(rate_by_ball * 100) + + def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball): + new_catch_rate_by_ball = [] + self.emit_event( + 'pokemon_catch_rate', + level='debug', + formatted='Catch rate of {catch_rate} with {ball_name} is low. Throwing {berry_name} (have {berry_count})', + data={ + 'catch_rate': self._pct(catch_rate_by_ball[current_ball]), + 'ball_name': self.item_list[str(current_ball)], + 'berry_name': self.item_list[str(berry_id)], + 'berry_count': berry_count + } + ) + + response_dict = self.api.use_item_capture( + item_id=berry_id, + encounter_id=encounter_id, + spawn_point_id=self.spawn_point_guid + ) + responses = response_dict['responses'] + + if response_dict and response_dict['status_code'] == 1: + + # update catch rates using multiplier + if 'item_capture_mult' in responses['USE_ITEM_CAPTURE']: + for rate in catch_rate_by_ball: + new_catch_rate_by_ball.append(rate * responses['USE_ITEM_CAPTURE']['item_capture_mult']) + self.emit_event( + 'threw_berry', + formatted="Threw a {berry_name}! Catch rate with {ball_name} is now: {new_catch_rate}", + data={ + 'berry_name': self.item_list[str(berry_id)], + 'ball_name': self.item_list[str(current_ball)], + 'new_catch_rate': self._pct(catch_rate_by_ball[current_ball]) + } + ) + + # softban? + else: + new_catch_rate_by_ball = catch_rate_by_ball + self.emit_event( + 'softban', + level='warning', + formatted='Failed to use berry. You may be softbanned.' + ) + + # unknown status code + else: + new_catch_rate_by_ball = catch_rate_by_ball + self.emit_event( + 'threw_berry_failed', + formatted='Unknown response when throwing berry: {status_code}.', + data={ + 'status_code': response_dict['status_code'] + } + ) + + return new_catch_rate_by_ball + + def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): + # settings that may be exposed at some point + berry_id = ITEM_RAZZBERRY + maximum_ball = ITEM_ULTRABALL if is_vip else ITEM_GREATBALL + ideal_catch_rate_before_throw = 0.9 if is_vip else 0.35 + + berry_count = self.bot.item_inventory_count(berry_id) + items_stock = self.bot.current_inventory() + + while True: + + # find lowest available ball + current_ball = ITEM_POKEBALL + while items_stock[current_ball] == 0 and current_ball < maximum_ball: + current_ball += 1 + if items_stock[current_ball] == 0: + self.emit_event('no_pokeballs', formatted='No usable pokeballs found!') + break + + # check future ball count + num_next_balls = 0 + next_ball = current_ball + while next_ball < maximum_ball: + next_ball += 1 + num_next_balls += items_stock[next_ball] + + # check if we've got berries to spare + berries_to_spare = berry_count > 0 if is_vip else berry_count > num_next_balls + 30 + + # use a berry if we are under our ideal rate and have berries to spare + used_berry = False + if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and berries_to_spare: + catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) + berry_count -= 1 + used_berry = True + + # pick the best ball to catch with + best_ball = current_ball + while best_ball < maximum_ball: + best_ball += 1 + if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and items_stock[best_ball] > 0: + # if current ball chance to catch is under our ideal rate, and player has better ball - then use it + current_ball = best_ball + + # if the rate is still low and we didn't throw a berry before, throw one + if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and berry_count > 0 and not used_berry: + catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) + berry_count -= 1 + + # get current pokemon list before catch + pokemon_before_catch = self._get_current_pokemon_ids() + + # try to catch pokemon! + items_stock[current_ball] -= 1 + self.emit_event( + 'threw_pokeball', + formatted='Used {ball_name}, with chance {success_percentage} ({count_left} left)', + data={ + 'ball_name': self.item_list[str(current_ball)], + 'success_percentage': self._pct(catch_rate_by_ball[current_ball]), + 'count_left': items_stock[current_ball] + } + ) + + reticle_size_parameter = normalized_reticle_size(self.config.catch_randomize_reticle_factor) + spin_modifier_parameter = spin_modifier(self.config.catch_randomize_spin_factor) + + response_dict = self.api.catch_pokemon( + encounter_id=encounter_id, + pokeball=current_ball, + 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 + ) + + try: + catch_pokemon_status = response_dict['responses']['CATCH_POKEMON']['status'] + except KeyError: + break + + # retry failed pokemon + if catch_pokemon_status == CATCH_STATUS_FAILED: + self.emit_event( + 'pokemon_capture_failed', + formatted='{pokemon} capture failed.. trying again!', + data={'pokemon': pokemon.name} + ) + sleep(2) + continue + + # abandon if pokemon vanished + elif catch_pokemon_status == CATCH_STATUS_VANISHED: + self.emit_event( + 'pokemon_vanished', + formatted='{pokemon} vanished!', + data={'pokemon': pokemon.name} + ) + if self._pct(catch_rate_by_ball[current_ball]) == 100: + self.bot.softban = True + + # pokemon caught! + elif catch_pokemon_status == CATCH_STATUS_SUCCESS: + self.bot.metrics.captured_pokemon(pokemon.name, pokemon.cp, pokemon.iv_display, pokemon.iv) + self.emit_event( + 'pokemon_caught', + formatted='Captured {pokemon}! [CP {cp}] [Potential {iv}] [{iv_display}] [+{exp} exp]', + data={ + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv, + 'iv_display': pokemon.iv_display, + '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 + if self.config.evolve_captured and (self.config.evolve_captured[0] == 'all' or pokemon.name in self.config.evolve_captured): + pokemon_after_catch = self._get_current_pokemon_ids() + pokemon_to_evolve = list(set(pokemon_after_catch) - set(pokemon_before_catch)) + + if len(pokemon_to_evolve) == 0: + break + + self._do_evolve(pokemon, pokemon_to_evolve[0]) + + break + + def _do_evolve(self, pokemon, new_pokemon_id): + response_dict = self.api.evolve_pokemon(pokemon_id=new_pokemon_id) + catch_pokemon_status = response_dict['responses']['EVOLVE_POKEMON']['result'] + + if catch_pokemon_status == 1: + self.emit_event( + 'pokemon_evolved', + formatted='{pokemon} evolved!', + data={'pokemon': pokemon.name} + ) + else: + self.emit_event( + 'pokemon_evolve_fail', + formatted='Failed to evolve {pokemon}!', + data={'pokemon': pokemon.name} + ) diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 2c969913b0..673b373fba 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -3,10 +3,12 @@ from pokemongo_bot.base_task import BaseTask from pokemongo_bot.tree_config_builder import ConfigException + class RecycleItems(BaseTask): SUPPORTED_TASK_API_VERSION = 1 def initialize(self): + self.min_empty_space = self.config.get('min_empty_space', None) self.item_filter = self.config.get('item_filter', {}) self._validate_item_filter() @@ -15,9 +17,26 @@ def _validate_item_filter(self): for config_item_name, bag_count in self.item_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)) + raise ConfigException( + "item {} does not exist, spelling mistake? (check for valid item names in data/items.json)".format( + config_item_name)) 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: + 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') @@ -58,7 +77,7 @@ def work(self): 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} + # {'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 diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index e04a86dbc6..445946e7e1 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -15,14 +15,16 @@ class SpinFort(BaseTask): SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): + self.ignore_item_count = self.config.get("ignore_item_count", False) + def should_run(self): if not self.bot.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 False - return True + return self.ignore_item_count or self.bot.has_space_for_loot() def work(self): fort = self.get_fort_in_range() @@ -139,6 +141,11 @@ def work(self): def get_fort_in_range(self): forts = self.bot.get_forts(order_by_distance=True) + for fort in forts: + if 'cooldown_complete_timestamp_ms' in fort: + self.bot.fort_timeouts[fort["id"]] = fort['cooldown_complete_timestamp_ms'] + forts.remove(fort) + forts = filter(lambda x: x["id"] not in self.bot.fort_timeouts, forts) if len(forts) == 0: diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 5c1d30fae7..ebc197ef24 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -43,17 +43,6 @@ def work(self): all_pokemons.remove(pokemon) best_pokemons.append(pokemon) - if best_pokemons and all_pokemons: - self.emit_event( - 'keep_best_release', - formatted="Keeping best {amount} {pokemon}, based on {criteria}", - data={ - 'amount': len(best_pokemons), - 'pokemon': pokemon_name, - 'criteria': order_criteria - } - ) - transfer_pokemons = [pokemon for pokemon in all_pokemons if self.should_release_pokemon(pokemon_name, pokemon['cp'], @@ -61,6 +50,16 @@ def work(self): True)] if transfer_pokemons: + if best_pokemons: + self.emit_event( + 'keep_best_release', + formatted="Keeping best {amount} {pokemon}, based on {criteria}", + data={ + 'amount': len(best_pokemons), + 'pokemon': pokemon_name, + 'criteria': order_criteria + } + ) for pokemon in transfer_pokemons: self.release_pokemon(pokemon_name, pokemon['cp'], pokemon['iv'], pokemon['pokemon_data']['id']) else: diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index acbfaa7fe4..0a6e3c592d 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -70,6 +70,8 @@ def __init__(self, bot, config): self.min_interval = self.DEFAULT_MIN_INTERVAL self.displayed_stats = self.DEFAULT_DISPLAYED_STATS + self.bot.event_manager.register_event('update_title', parameters=('title')) + self._process_config() def initialize(self): @@ -109,6 +111,15 @@ def _update_title(self, title, platform): :rtype: None :raise: RuntimeError: When the given platform isn't supported. """ + + self.emit_event( + 'update_title', + formatted="{title}", + data={ + 'title': title + } + ) + if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) stdout.flush() diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py new file mode 100644 index 0000000000..c74a85296f --- /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 diff --git a/pokemongo_bot/step_walker.py b/pokemongo_bot/step_walker.py index 263699095d..f6c2cbfe96 100644 --- a/pokemongo_bot/step_walker.py +++ b/pokemongo_bot/step_walker.py @@ -39,6 +39,7 @@ def __init__(self, bot, speed, dest_lat, dest_lng): def step(self): if (self.dLat == 0 and self.dLng == 0) or self.dist < self.speed: self.api.set_position(self.destLat, self.destLng, 0) + self.bot.heartbeat() return True totalDLat = (self.destLat - self.initLat) diff --git a/pokemongo_bot/worker_result.py b/pokemongo_bot/worker_result.py index f38ceb9704..0e3ba10ebd 100644 --- a/pokemongo_bot/worker_result.py +++ b/pokemongo_bot/worker_result.py @@ -1,3 +1,4 @@ class WorkerResult(object): RUNNING = 'RUNNING' SUCCESS = 'SUCCESS' + ERROR = 'ERROR' diff --git a/requirements.txt b/requirements.txt index f6a22a0233..76b1d15a7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy==1.11.0 networkx==1.11 --e git+https://github.com/keyphact/pgoapi.git@249d3be7fbbdabc7f9adea17cbc899d6549e47a2#egg=pgoapi +-e git+https://github.com/keyphact/pgoapi.git@a2755eb42dfe49e359798d2f4defefc97fb8163d#egg=pgoapi geopy==1.11.0 protobuf==3.0.0b4 requests==2.10.0 diff --git a/run.sh b/run.sh index ec95acb3e3..0812dfac8c 100755 --- a/run.sh +++ b/run.sh @@ -1,18 +1,21 @@ #!/usr/bin/env bash - -# Starts PokemonGo-Bot -config="" - +pokebotpath=$(pwd) +filename="" if [ ! -z $1 ]; then - config=$1 +filename=$1 else - config="./configs/config.json" - if [ ! -f ${config} ]; then - echo -e "There's no ./configs/config.json file" - echo -e "Please create one or use another config file" - echo -e "./run.sh [path/to/config/file]" - exit 1 - fi +filename="./configs/config.json" +if [ ! -f "$filename" ] +then +echo "There's no "$filename" file. use setup.sh -config to creat one." +fi fi -python pokecli.py --config ${config} +while true +do +cd $pokebotpath +python pokecli.py -cf $filename +read -p "Press any button or wait 20 seconds." -r -s -n1 -t 20 +echo `date`"Pokebot"$*" Stopped." +done +exit 0 diff --git a/setup.py b/setup.py deleted file mode 100644 index 6ccf893c0d..0000000000 --- a/setup.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python - -from pip.req import parse_requirements - -install_reqs = parse_requirements("requirements.txt", session=False) - -reqs = [str(ir.req) for ir in install_reqs] - -setup(name='pgoapi', - version='1.0', - url='https://github.com/tejado/pgoapi', - packages=['pgoapi'], - install_requires=reqs) diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000000..bcff0feefb --- /dev/null +++ b/setup.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +pokebotpath=$(pwd) +backuppath=$(pwd)"/backup" + +function Pokebotupdate () { +cd $pokebotpath +git pull +git submodule init +git submodule foreach git pull origin master +virtualenv . +source bin/activate +pip install -r requirements.txt +} + +function Pokebotencrypt () { +echo "Start to make encrypt.so" +wget http://pgoapi.com/pgoencrypt.tar.gz +tar -xf pgoencrypt.tar.gz +cd pgoencrypt/src/ +make +mv libencrypt.so $pokebotpath/encrypt.so +cd ../.. +rm -rf pgoencrypt.tar.gz +rm -rf pgoencrypt +} + +function Pokebotconfig () { +cd $pokebotpath +read -p "1.google 2.ptc +" auth +read -p "Input username +" username +read -p "Input password +" -s password +read -p " +Input location +" location +read -p "Input gmapkey +" gmapkey +cp configs/config.json.example configs/config.json +if [ "$auth" = "2" ] +then +sed -i "s/google/ptc/g" configs/config.json +fi +sed -i "s/YOUR_USERNAME/$username/g" configs/config.json +sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json +sed -i "s/SOME_LOCATION/$location/g" configs/config.json +sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json +echo "Edit configs/config.json to modify any other config." +} + +function Pokebotinstall () { +cd $pokebotpath +if [ -f /etc/debian_version ] +then +echo "You are on Debian/Ubuntu" +sudo apt-get update +sudo apt-get -y install python python-pip python-dev build-essential git python-protobuf virtualenv +elif [ -f /etc/redhat-release ] +then +echo "You are on CentOS/RedHat" +sudo yum -y install epel-release +sudo yum -y install python-pip +elif [ "$(uname -s)" == "Darwin" ] +then +echo "You are on Mac os" +sudo brew update +sudo brew install --devel protobuf +else +echo "Nothing happend." +fi +sudo pip install virtualenv +Pokebotupdate +Pokebotencrypt +echo "Install complete." +Pokebotconfig +} + +function Pokebotreset () { +cd $pokebotpath +git fetch --all +git reset --hard origin/dev +Pokebotupdate +} + +function Pokebothelp () { +echo "usage:" +echo " -i,--install. Install PokemonGo-Bot." +echo " -b,--backup. Backup config files." +echo " -c,--config. Easy config generator." +echo " -e,--encrypt. Make encrypt.so." +echo " -r,--reset. Force sync dev branch." +echo " -u,--update. Command git pull to update." +} + +case $* in +--install|-i) +Pokebotinstall +;; +--encrypt|-e) +Pokebotencrypt +;; +--reset|-r) +Pokebotreset +;; +--update|-u) +Pokebotupdate +;; +--backup|-b) +mkdir $backuppath +cp -f $pokebotpath/configs/config*.json $backuppath/ +cp -f $pokebotpath/web/config/userdata.js $backuppath/ +echo "Backup complete" +;; +--config|-c) +Pokebotconfig +;; +--help|-h) +Pokebothelp +;; +*.json) +filename=$* +cd $pokebotpath +if [ ! -f ./configs/"$filename" ] +then +echo "There's no ./configs/"$filename" file. It's better to use run.sh not this one." +else +Pokebotrun +fi +;; +*) +Pokebothelp +;; +esac +exit 0 From 234baa4331f5ddb66af9171b1b9c4184238178df Mon Sep 17 00:00:00 2001 From: mjmadsen Date: Tue, 9 Aug 2016 13:28:37 -0500 Subject: [PATCH 116/202] Checking config file exists in run.sh (#3326) --- run.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/run.sh b/run.sh index 0812dfac8c..d8406697e7 100755 --- a/run.sh +++ b/run.sh @@ -5,10 +5,10 @@ if [ ! -z $1 ]; then filename=$1 else filename="./configs/config.json" -if [ ! -f "$filename" ] -then -echo "There's no "$filename" file. use setup.sh -config to creat one." fi + +if [ ! -f "$filename" ]; then +echo "There's no "$filename" file. use setup.sh -config to creat one." fi while true From afb139d61fc911aa1f8df58dd6e858ce1c644c09 Mon Sep 17 00:00:00 2001 From: Amal Samally Date: Tue, 9 Aug 2016 22:33:33 +0400 Subject: [PATCH 117/202] Improve and update pokemon.json (#3331) 1. Unminify for simplier edits 2. Add BaseAttack, BaseDefense, BaseStamina, CaptureRate, FleeRate, Fast/Special Attack(s) --- data/pokemon.json | 5895 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 5894 insertions(+), 1 deletion(-) diff --git a/data/pokemon.json b/data/pokemon.json index a227106841..58f7c437f2 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -1 +1,5894 @@ -[{"Number":"001","Name":"Bulbasaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Tackle","Vine Whip"],"Weight":"6.9 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"002","Name":"Ivysaur"},{"Number":"003","Name":"Venusaur"}]},{"Number":"002","Name":"Ivysaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"13.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"003","Name":"Venusaur"}]},{"Number":"003","Name":"Venusaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"100.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"},{"Number":"002","Name":"Ivysaur"}]},{"Number":"004","Name":"Charmander","Classification":"Lizard Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Scratch"],"Weight":"8.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"005","Name":"Charmeleon"},{"Number":"006","Name":"Charizard"}]},{"Number":"005","Name":"Charmeleon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"19.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"}],"Next Evolution Requirements":{"Amount":100,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"006","Name":"Charizard"}]},{"Number":"006","Name":"Charizard","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Ember","Wing Attack"],"Weight":"90.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"},{"Number":"005","Name":"Charmeleon"}]},{"Number":"007","Name":"Squirtle","Classification":"Tiny Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Tackle","Bubble"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"008","Name":"Wartortle"},{"Number":"009","Name":"Blastoise"}]},{"Number":"008","Name":"Wartortle","Classification":"Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"22.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"}],"Next Evolution Requirements":{"Amount":100,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"009","Name":"Blastoise"}]},{"Number":"009","Name":"Blastoise","Classification":"Shellfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"85.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"},{"Number":"008","Name":"Wartortle"}]},{"Number":"010","Name":"Caterpie","Classification":"Worm Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"2.9 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"011","Name":"Metapod"},{"Number":"012","Name":"Butterfree"}]},{"Number":"011","Name":"Metapod","Classification":"Cocoon Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"9.9 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"}],"Next Evolution Requirements":{"Amount":50,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"012","Name":"Butterfree"}]},{"Number":"012","Name":"Butterfree","Classification":"Butterfly Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"32.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"},{"Number":"011","Name":"Metapod"}]},{"Number":"013","Name":"Weedle","Classification":"Hairy Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Sting"],"Weight":"3.2 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"014","Name":"Kakuna"},{"Number":"015","Name":"Beedrill"}]},{"Number":"014","Name":"Kakuna","Classification":"Cocoon Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Posion Sting"],"Weight":"10.0 kg","Height":"0.6 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"}],"Next Evolution Requirements":{"Amount":50,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"015","Name":"Beedrill"}]},{"Number":"015","Name":"Beedrill","Classification":"Poison Bee Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Jab"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"},{"Number":"014","Name":"Kakuna"}]},{"Number":"016","Name":"Pidgey","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"1.8 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"017","Name":"Pidgeotto"},{"Number":"018","Name":"Pidgeot"}]},{"Number":"017","Name":"Pidgeotto","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"30.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"}],"Next Evolution Requirements":{"Amount":50,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"018","Name":"Pidgeot"}]},{"Number":"018","Name":"Pidgeot","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Hurricane"],"Weight":"39.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"},{"Number":"017","Name":"Pidgeotto"}]},{"Number":"019","Name":"Rattata","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Body Slam","Dig","Hyper Fang"],"Weight":"3.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Rattata candies"},"Next evolution(s)":[{"Number":"020","Name":"Raticate"}]},{"Number":"020","Name":"Raticate","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Quick Attack"],"Special Attack(s)":["Dig","Hyper Beam","Hyper Fang"],"Weight":"18.5 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"019","Name":"Rattata"}]},{"Number":"021","Name":"Spearow","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"2.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Spearow candies"},"Next evolution(s)":[{"Number":"022","Name":"Fearow"}]},{"Number":"022","Name":"Fearow","Classification":"Beak Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Steel Wing"],"Weight":"38.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"021","Name":"Spearow"}]},{"Number":"023","Name":"Ekans","Classification":"Snake Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Sting"],"Weight":"6.9 kg","Height":"2.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ekans candies"},"Next evolution(s)":[{"Number":"024","Name":"Arbok"}]},{"Number":"024","Name":"Arbok","Classification":"Cobra Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Bite"],"Weight":"65.0 kg","Height":"3.5 m","Previous evolution(s)":[{"Number":"023","Name":"Ekans"}]},{"Number":"025","Name":"Pikachu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Quick Attack","Thunder Shock"],"Weight":"6.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Pikachu candies"},"Next evolution(s)":[{"Number":"026","Name":"Raichu"}]},{"Number":"026","Name":"Raichu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock","Spark"],"Weight":"30.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"025","Name":"Pikachu"}]},{"Number":"027","Name":"Sandshrew","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"12.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Sandshrew candies"},"Next evolution(s)":[{"Number":"028","Name":"Sandslash"}]},{"Number":"028","Name":"Sandslash","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"027","Name":"Sandshrew"}]},{"Number":"029","Name":"Nidoran F","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"7.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"030","Name":"Nidorina"},{"Number":"031","Name":"Nidoqueen"}]},{"Number":"030","Name":"Nidorina","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"20.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"031","Name":"Nidoqueen"}]},{"Number":"031","Name":"Nidoqueen","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"},{"Number":"030","Name":"Nidorina"}]},{"Number":"032","Name":"Nidoran M","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Peck","Poison Sting"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"033","Name":"Nidorino"},{"Number":"034","Name":"Nidoking"}]},{"Number":"033","Name":"Nidorino","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"19.5 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"034","Name":"Nidoking"}]},{"Number":"034","Name":"Nidoking","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Fury Cutter","Poison Jab"],"Weight":"62.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"},{"Number":"033","Name":"Nidorino"}]},{"Number":"035","Name":"Clefairy","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"7.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Clefairy candies"},"Next evolution(s)":[{"Number":"036","Name":"Clefable"}]},{"Number":"036","Name":"Clefable","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"40.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"035","Name":"Clefairy"}]},{"Number":"037","Name":"Vulpix","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"9.9 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Vulpix candies"},"Next evolution(s)":[{"Number":"038","Name":"Ninetales"}]},{"Number":"038","Name":"Ninetales","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"19.9 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"037","Name":"Vulpix"}]},{"Number":"039","Name":"Jigglypuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"5.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Jigglypuff candies"},"Next evolution(s)":[{"Number":"039","Name":"Jigglypuff"}]},{"Number":"040","Name":"Wigglytuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"12.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"040","Name":"Wigglytuff"}]},{"Number":"041","Name":"Zubat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Quick Attack"],"Weight":"7.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Zubat candies"},"Next evolution(s)":[{"Number":"042","Name":"Golbat"}]},{"Number":"042","Name":"Golbat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Wing Attack"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"041","Name":"Zubat"}]},{"Number":"043","Name":"Oddish","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"5.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"044","Name":"Gloom"},{"Number":"045","Name":"Vileplume"}]},{"Number":"044","Name":"Gloom","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"8.6 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"}],"Next Evolution Requirements":{"Amount":100,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"045","Name":"Vileplume"}]},{"Number":"045","Name":"Vileplume","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid",""],"Weight":"18.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"},{"Number":"044","Name":"Gloom"}]},{"Number":"046","Name":"Paras","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Scratch"],"Weight":"5.4 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Paras candies"},"Next evolution(s)":[{"Number":"047","Name":"Parasect"}]},{"Number":"047","Name":"Parasect","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Fury Cutter"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"046","Name":"Paras"}]},{"Number":"048","Name":"Venonat","Classification":"Insect Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Venonat candies"},"Next evolution(s)":[{"Number":"049","Name":"Venomoth"}]},{"Number":"049","Name":"Venomoth","Classification":"Poison Moth Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"12.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"048","Name":"Venonat"}]},{"Number":"050","Name":"Diglett","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"0.8 kg","Height":"0.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Diglett candies"},"Next evolution(s)":[{"Number":"051","Name":"Dugtrio"}]},{"Number":"051","Name":"Dugtrio","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Sucker Punch"],"Weight":"33.3 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"050","Name":"Diglett"}]},{"Number":"052","Name":"Meowth","Classification":"Scratch Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Scratch"],"Weight":"4.2 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Meowth candies"},"Next evolution(s)":[{"Number":"053","Name":"Persian"}]},{"Number":"053","Name":"Persian","Classification":"Classy Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Scratch"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"052","Name":"Meowth"}]},{"Number":"054","Name":"Psyduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun","Zen Headbutt"],"Weight":"19.6 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Psyduck candies"},"Next evolution(s)":[{"Number":"055","Name":"Golduck"}]},{"Number":"055","Name":"Golduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"76.6 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"054","Name":"Psyduck"}]},{"Number":"056","Name":"Mankey","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Scratch"],"Weight":"28.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Mankey candies"},"Next evolution(s)":[{"Number":"057","Name":"Primeape"}]},{"Number":"057","Name":"Primeape","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"056","Name":"Mankey"}]},{"Number":"058","Name":"Growlithe","Classification":"Puppy Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Ember"],"Weight":"19.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":50,"Name":"Growlithe candies"},"Next evolution(s)":[{"Number":"059","Name":"Arcanine"}]},{"Number":"059","Name":"Arcanine","Classification":"Legendary Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Fire Fang"],"Weight":"155.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"058","Name":"Growlithe"}]},{"Number":"060","Name":"Poliwag","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"12.4 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"061","Name":"Poliwhirl"},{"Number":"062","Name":"Poliwrath"}]},{"Number":"061","Name":"Poliwhirl","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"20.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"}],"Next Evolution Requirements":{"Amount":100,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"062","Name":"Poliwrath"}]},{"Number":"062","Name":"Poliwrath","Classification":"Tadpole Pokemon","Type I":["Water"],"Type II":["Fighting"],"Weaknesses":["Electric","Grass","Flying","Psychic","Fairy"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"54.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"},{"Number":"061","Name":"Poliwhirl"}]},{"Number":"063","Name":"Abra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Zen Headbutt",""],"Weight":"19.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":25,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"064","Name":"Kadabra"},{"Number":"065","Name":"Alakazam"}]},{"Number":"064","Name":"Kadabra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"56.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"}],"Next Evolution Requirements":{"Amount":100,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"065","Name":"Alakazam"}]},{"Number":"065","Name":"Alakazam","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"48.0 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"},{"Number":"064","Name":"Kadabra"}]},{"Number":"066","Name":"Machop","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"19.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"067","Name":"Machoke"},{"Number":"068","Name":"Machamp"}]},{"Number":"067","Name":"Machoke","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"70.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"}],"Next Evolution Requirements":{"Amount":100,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"068","Name":"Machamp"}]},{"Number":"068","Name":"Machamp","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Karate Chop"],"Weight":"130.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"},{"Number":"067","Name":"Machoke"}]},{"Number":"069","Name":"Bellsprout","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Vine Whip"],"Weight":"4.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"070","Name":"Weepinbell"},{"Number":"071","Name":"Victreebel"}]},{"Number":"070","Name":"Weepinbell","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"6.4 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"071","Name":"Victreebel"}]},{"Number":"071","Name":"Victreebel","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"15.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"},{"Number":"070","Name":"Weepinbell"}]},{"Number":"072","Name":"Tentacool","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Bubble","Poison Sting"],"Weight":"45.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Tentacool candies"},"Next evolution(s)":[{"Number":"073","Name":"Tentacruel"}]},{"Number":"073","Name":"Tentacruel","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Jab"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"072","Name":"Tentacool"}]},{"Number":"074","Name":"Geodude","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"20.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"075","Name":"Graveler"},{"Number":"076","Name":"Golem"}]},{"Number":"075","Name":"Graveler","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"105.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"}],"Next Evolution Requirements":{"Amount":100,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"076","Name":"Golem"}]},{"Number":"076","Name":"Golem","Classification":"Megaton Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"300.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"},{"Number":"075","Name":"Graveler"}]},{"Number":"077","Name":"Ponyta","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Tackle"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ponyta candies"},"Next evolution(s)":[{"Number":"078","Name":"Rapidash"}]},{"Number":"078","Name":"Rapidash","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Low Kick"],"Weight":"95.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"077","Name":"Ponyta"}]},{"Number":"079","Name":"Slowpoke","Classification":"Dopey Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"36.0 kg","Height":"1.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Slowpoke candies"},"Next evolution(s)":[{"Number":"080","Name":"Slowbro"}]},{"Number":"080","Name":"Slowbro","Classification":"Hermit Crab Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"78.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"079","Name":"Slowpoke"}]},{"Number":"081","Name":"Magnemite","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"6.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Magnemite candies"},"Next evolution(s)":[{"Number":"082","Name":"Magneton"}]},{"Number":"082","Name":"Magneton","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"60.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"081","Name":"Magnemite"}]},{"Number":"083","Name":"Farfetch'd","Classification":"Wild Duck Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"15.0 kg","Height":"0.8 m"},{"Number":"084","Name":"Doduo","Classification":"Twin Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"39.2 kg","Height":"1.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Doduo candies"},"Next evolution(s)":[{"Number":"085","Name":"Dodrio"}]},{"Number":"085","Name":"Dodrio","Classification":"Triple Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Feint Attack","Steel Wing"],"Weight":"85.2 kg","Height":"1.8 m","Previous evolution(s)":[{"Number":"084","Name":"Doduo"}]},{"Number":"086","Name":"Seel","Classification":"Sea Lion Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Water Gun"],"Weight":"90.0 kg","Height":"1.1 m","Next Evolution Requirements":{"Amount":50,"Name":"Seel candies"},"Next evolution(s)":[{"Number":"087","Name":"Dewgong"}]},{"Number":"087","Name":"Dewgong","Classification":"Sea Lion Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"120.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"086","Name":"Seel"}]},{"Number":"088","Name":"Grimer","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Mud Slap"],"Weight":"30.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Grimer candies"},"Next evolution(s)":[{"Number":"089","Name":"Muk"}]},{"Number":"089","Name":"Muk","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Poison Jab",""],"Weight":"30.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"088","Name":"Grimer"}]},{"Number":"090","Name":"Shellder","Classification":"Bivalve Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Tackle"],"Weight":"4.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Shellder candies"},"Next evolution(s)":[{"Number":"091","Name":"Cloyster"}]},{"Number":"091","Name":"Cloyster","Classification":"Bivalve Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"132.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"090","Name":"Shellder"}]},{"Number":"092","Name":"Gastly","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Sucker Punch"],"Weight":"0.1 kg","Height":"1.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"093","Name":"Haunter"},{"Number":"094","Name":"Gengar"}]},{"Number":"093","Name":"Haunter","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Shadow Claw"],"Weight":"0.1 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"}],"Next Evolution Requirements":{"Amount":100,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"094","Name":"Gengar"}]},{"Number":"094","Name":"Gengar","Classification":"Shadow Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Shadow Claw","Sucker Punch"],"Weight":"40.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"},{"Number":"093","Name":"Haunter"}]},{"Number":"095","Name":"Onix","Classification":"Rock Snake Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"210.0 kg","Height":"8.8 m"},{"Number":"096","Name":"Drowzee","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Pound"],"Weight":"32.4 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Drowzee candies"},"Next evolution(s)":[{"Number":"097","Name":"Hypno"}]},{"Number":"097","Name":"Hypno","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"75.6 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"096","Name":"Drowzee"}]},{"Number":"098","Name":"Krabby","Classification":"River Crab Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Krabby candies"},"Next evolution(s)":[{"Number":"099","Name":"Kingler"}]},{"Number":"099","Name":"Kingler","Classification":"Pincer Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"098","Name":"Krabby"}]},{"Number":"100","Name":"Voltorb","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark","Tackle"],"Weight":"10.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Voltorb candies"},"Next evolution(s)":[{"Number":"101","Name":"Electrode"}]},{"Number":"101","Name":"Electrode","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark",""],"Weight":"66.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"100","Name":"Voltorb"}]},{"Number":"102","Name":"Exeggcute","Classification":"Egg Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion",""],"Weight":"2.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Exeggcute candies"},"Next evolution(s)":[{"Number":"103","Name":"Exeggutor"}]},{"Number":"103","Name":"Exeggutor","Classification":"Coconut Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"120.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"102","Name":"Exeggcute"}]},{"Number":"104","Name":"Cubone","Classification":"Lonely Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Cubone candies"},"Next evolution(s)":[{"Number":"105","Name":"Marowak"}]},{"Number":"105","Name":"Marowak","Classification":"Bone Keeper Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"45.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"104","Name":"Cubone"}]},{"Number":"106","Name":"Hitmonlee","Classification":"Kicking Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Low Kick","Rock Smash"],"Weight":"49.8 kg","Height":"1.5 m","Next evolution(s)":[{"Number":"107","Name":"Hitmonchan"}]},{"Number":"107","Name":"Hitmonchan","Classification":"Punching Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Rock Smash"],"Weight":"50.2 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"106","Name":"Hitmonlee"}]},{"Number":"108","Name":"Lickitung","Classification":"Licking Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"65.5 kg","Height":"1.2 m"},{"Number":"109","Name":"Koffing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"1.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Koffing candies"},"Next evolution(s)":[{"Number":"110","Name":"Weezing"}]},{"Number":"110","Name":"Weezing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"9.5 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"109","Name":"Koffing"}]},{"Number":"111","Name":"Rhyhorn","Classification":"Spikes Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"115.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Rhyhorn candies"},"Next evolution(s)":[{"Number":"112","Name":"Rhydon"}]},{"Number":"112","Name":"Rhydon","Classification":"Drill Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"120.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"111","Name":"Rhyhorn"}]},{"Number":"113","Name":"Chansey","Classification":"Egg Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"34.6 kg","Height":"1.1 m"},{"Number":"114","Name":"Tangela","Classification":"Vine Pokemon","Type I":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug"],"Fast Attack(s)":["Vine Whip",""],"Weight":"35.0 kg","Height":"1.0 m"},{"Number":"115","Name":"Kangaskhan","Classification":"Parent Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Low Kick",""],"Weight":"80.0 kg","Height":"2.2 m"},{"Number":"116","Name":"Horsea","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Water Gun"],"Weight":"8.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Horsea candies"},"Next evolution(s)":[{"Number":"117","Name":"Seadra"}]},{"Number":"117","Name":"Seadra","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Dragon Breath","Water Gun"],"Weight":"25.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"116","Name":"Horsea"}]},{"Number":"118","Name":"Goldeen","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Mud Shot"],"Weight":"15.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Goldeen candies"},"Next evolution(s)":[{"Number":"119","Name":"Seaking"}]},{"Number":"119","Name":"Seaking","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Poison Jab"],"Weight":"39.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"118","Name":"Goldeen"}]},{"Number":"120","Name":"Staryu","Classification":"Starshape Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"34.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Staryu candies"},"Next evolution(s)":[{"Number":"120","Name":"Staryu"}]},{"Number":"121","Name":"Starmie","Classification":"Mysterious Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"80.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"121","Name":"Starmie"}]},{"Number":"122","Name":"Mr. Mime","Classification":"Barrier Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"54.5 kg","Height":"1.3 m"},{"Number":"123","Name":"Scyther","Classification":"Mantis Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Steel Wing"],"Weight":"56.0 kg","Height":"1.5 m"},{"Number":"124","Name":"Jynx","Classification":"Humanshape Pokemon","Type I":["Ice"],"Type II":["Psychic"],"Weaknesses":["Fire","Bug","Rock","Ghost","Dark","Steel"],"Fast Attack(s)":["Frost Breath","Pound"],"Weight":"40.6 kg","Height":"1.4 m"},{"Number":"125","Name":"Electabuzz","Classification":"Electric Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Low Kick","Thunder Shock"],"Weight":"30.0 kg","Height":"1.1 m"},{"Number":"126","Name":"Magmar","Classification":"Spitfire Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Karate Chop"],"Weight":"44.5 kg","Height":"1.3 m"},{"Number":"127","Name":"Pinsir","Classification":"Stagbeetle Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Rock Smash"],"Weight":"55.0 kg","Height":"1.5 m"},{"Number":"128","Name":"Tauros","Classification":"Wild Bull Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Tackle","Zen Headbutt"],"Weight":"88.4 kg","Height":"1.4 m"},{"Number":"129","Name":"Magikarp","Classification":"Fish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Splash",""],"Weight":"10.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":400,"Name":"Magikarp candies"},"Next evolution(s)":[{"Number":"130","Name":"Gyarados"}]},{"Number":"130","Name":"Gyarados","Classification":"Atrocious Pokemon","Type I":["Water"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Bite","Dragon Breath"],"Weight":"235.0 kg","Height":"6.5 m","Previous evolution(s)":[{"Number":"129","Name":"Magikarp"}]},{"Number":"131","Name":"Lapras","Classification":"Transport Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"220.0 kg","Height":"2.5 m"},{"Number":"132","Name":"Ditto","Classification":"Transform Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.3 m"},{"Number":"133","Name":"Eevee","Classification":"Evolution Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"6.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Eevee candies"},"Next evolution(s)":[{"Number":"134","Name":"Vaporeon"},{"Number":"135","Name":"Jolteon"},{"Number":"136","Name":"Flareon"}]},{"Number":"134","Name":"Vaporeon","Classification":"Bubble Jet Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun",""],"Weight":"29.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"135","Name":"Jolteon","Classification":"Lightning Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock",""],"Weight":"24.5 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"136","Name":"Flareon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"25.0 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"137","Name":"Porygon","Classification":"Virtual Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"36.5 kg","Height":"0.8 m"},{"Number":"138","Name":"Omanyte","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Water Gun",""],"Weight":"7.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Omanyte candies"},"Next evolution(s)":[{"Number":"139","Name":"Omastar"}]},{"Number":"139","Name":"Omastar","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Rock Throw","Water Gun"],"Weight":"35.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"138","Name":"Omanyte"}]},{"Number":"140","Name":"Kabuto","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"11.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Kabuto candies"},"Next evolution(s)":[{"Number":"141","Name":"Kabutops"}]},{"Number":"141","Name":"Kabutops","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Fury Cutter","Mud Shot"],"Weight":"40.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"140","Name":"Kabuto"}]},{"Number":"142","Name":"Aerodactyl","Classification":"Fossil Pokemon","Type I":["Rock"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Ice","Rock","Steel"],"Fast Attack(s)":["Bite","Steel Wing"],"Weight":"59.0 kg","Height":"1.8 m"},{"Number":"143","Name":"Snorlax","Classification":"Sleeping Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"460.0 kg","Height":"2.1 m"},{"Number":"144","Name":"Articuno","Classification":"Freeze Pokemon","Type I":["Ice"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Rock","Steel"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"55.4 kg","Height":"1.7 m"},{"Number":"145","Name":"Zapdos","Classification":"Electric Pokemon","Type I":["Electric"],"Type II":["Flying"],"Weaknesses":["Ice","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"52.6 kg","Height":"1.6 m"},{"Number":"146","Name":"Moltres","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"60.0 kg","Height":"2.0 m"},{"Number":"147","Name":"Dratini","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"3.3 kg","Height":"1.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Dratini candies"}},{"Number":"148","Name":"Dragonair","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"16.5 kg","Height":"4.0 m","Next Evolution Requirements":{"Amount":100,"Name":"Dratini candies"},"Next evolution(s)":[{"Number":"149","Name":"Dragonite"}]},{"Number":"149","Name":"Dragonite","Classification":"Dragon Pokemon","Type I":["Dragon"],"Type II":["Flying"],"Weaknesses":["Ice","Rock","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath","Steel Wing"],"Weight":"210.0 kg","Height":"2.2 m","Previous evolution(s)":[{"Number":"148","Name":"Dragonair"}]},{"Number":"150","Name":"Mewtwo","Classification":"Genetic Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"122.0 kg","Height":"2.0 m"},{"Number":"151","Name":"Mew","Classification":"New Species Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.4 m"}] \ No newline at end of file +[ + { + "Number": "001", + "Name": "Bulbasaur", + "Classification": "Seed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Tackle", + "Vine Whip" + ], + "Weight": "6.9 kg", + "Height": "0.7 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 1, + "Name": "Bulbasaur candies" + }, + "Next evolution(s)": [ + { + "Number": "002", + "Name": "Ivysaur" + }, + { + "Number": "003", + "Name": "Venusaur" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Seed Bomb", + "Sludge Bomb" + ], + "BaseAttack": 126, + "BaseDefense": 90, + "BaseStamina": 126, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "002", + "Name": "Ivysaur", + "Classification": "Seed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Razor Leaf", + "Vine Whip" + ], + "Weight": "13.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "001", + "Name": "Bulbasaur" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 1, + "Name": "Bulbasaur candies" + }, + "Next evolution(s)": [ + { + "Number": "003", + "Name": "Venusaur" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 156, + "BaseDefense": 120, + "BaseStamina": 158, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "003", + "Name": "Venusaur", + "Classification": "Seed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Razor Leaf", + "Vine Whip" + ], + "Weight": "100.0 kg", + "Height": "2.0 m", + "Previous evolution(s)": [ + { + "Number": "001", + "Name": "Bulbasaur" + }, + { + "Number": "002", + "Name": "Ivysaur" + } + ], + "Special Attack(s)": [ + "Petal Blizzard", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 198, + "BaseDefense": 160, + "BaseStamina": 200, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "004", + "Name": "Charmander", + "Classification": "Lizard Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Scratch" + ], + "Weight": "8.5 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 4, + "Name": "Charmander candies" + }, + "Next evolution(s)": [ + { + "Number": "005", + "Name": "Charmeleon" + }, + { + "Number": "006", + "Name": "Charizard" + } + ], + "Special Attack(s)": [ + "Flame Burst", + "Flame Charge", + "Flamethrower" + ], + "BaseAttack": 128, + "BaseDefense": 78, + "BaseStamina": 108, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "005", + "Name": "Charmeleon", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Scratch" + ], + "Weight": "19.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "004", + "Name": "Charmander" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 4, + "Name": "Charmander candies" + }, + "Next evolution(s)": [ + { + "Number": "006", + "Name": "Charizard" + } + ], + "Special Attack(s)": [ + "Fire Punch", + "Flame Burst", + "Flamethrower" + ], + "BaseAttack": 160, + "BaseDefense": 116, + "BaseStamina": 140, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "006", + "Name": "Charizard", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Water", + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Wing Attack" + ], + "Weight": "90.5 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "004", + "Name": "Charmander" + }, + { + "Number": "005", + "Name": "Charmeleon" + } + ], + "Special Attack(s)": [ + "Dragon Claw", + "Fire Blast", + "Flamethrower" + ], + "BaseAttack": 212, + "BaseDefense": 156, + "BaseStamina": 182, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "007", + "Name": "Squirtle", + "Classification": "Tiny Turtle Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Tackle" + ], + "Weight": "9.0 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 7, + "Name": "Squirtle candies" + }, + "Next evolution(s)": [ + { + "Number": "008", + "Name": "Wartortle" + }, + { + "Number": "009", + "Name": "Blastoise" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Aqua Tail", + "Water Pulse" + ], + "BaseAttack": 112, + "BaseDefense": 88, + "BaseStamina": 142, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "008", + "Name": "Wartortle", + "Classification": "Turtle Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bite", + "Water Gun" + ], + "Weight": "22.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "007", + "Name": "Squirtle" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 7, + "Name": "Squirtle candies" + }, + "Next evolution(s)": [ + { + "Number": "009", + "Name": "Blastoise" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Hydro Pump", + "Ice Beam" + ], + "BaseAttack": 144, + "BaseDefense": 118, + "BaseStamina": 176, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "009", + "Name": "Blastoise", + "Classification": "Shellfish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bite", + "Water Gun" + ], + "Weight": "85.5 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "007", + "Name": "Squirtle" + }, + { + "Number": "008", + "Name": "Wartortle" + } + ], + "Special Attack(s)": [ + "Flash Cannon", + "Hydro Pump", + "Ice Beam" + ], + "BaseAttack": 186, + "BaseDefense": 158, + "BaseStamina": 222, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "010", + "Name": "Caterpie", + "Classification": "Worm Pokemon", + "Type I": [ + "Bug" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Tackle" + ], + "Weight": "2.9 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 12, + "Family": 10, + "Name": "Caterpie candies" + }, + "Next evolution(s)": [ + { + "Number": "011", + "Name": "Metapod" + }, + { + "Number": "012", + "Name": "Butterfree" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 62, + "BaseDefense": 90, + "BaseStamina": 66, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "011", + "Name": "Metapod", + "Classification": "Cocoon Pokemon", + "Type I": [ + "Bug" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Tackle" + ], + "Weight": "9.9 kg", + "Height": "0.7 m", + "Previous evolution(s)": [ + { + "Number": "010", + "Name": "Caterpie" + } + ], + "Next Evolution Requirements": { + "Amount": 50, + "Family": 10, + "Name": "Caterpie candies" + }, + "Next evolution(s)": [ + { + "Number": "012", + "Name": "Butterfree" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 56, + "BaseDefense": 100, + "BaseStamina": 86, + "CaptureRate": 0.2, + "FleeRate": 0.09 + }, + { + "Number": "012", + "Name": "Butterfree", + "Classification": "Butterfly Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Fire", + "Electric", + "Ice", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Confusion" + ], + "Weight": "32.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "010", + "Name": "Caterpie" + }, + { + "Number": "011", + "Name": "Metapod" + } + ], + "Special Attack(s)": [ + "Bug Buzz", + "Psychic", + "Signal Beam" + ], + "BaseAttack": 144, + "BaseDefense": 120, + "BaseStamina": 144, + "CaptureRate": 0.1, + "FleeRate": 0.06 + }, + { + "Number": "013", + "Name": "Weedle", + "Classification": "Hairy Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Poison Sting" + ], + "Weight": "3.2 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 12, + "Family": 13, + "Name": "Weedle candies" + }, + "Next evolution(s)": [ + { + "Number": "014", + "Name": "Kakuna" + }, + { + "Number": "015", + "Name": "Beedrill" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 68, + "BaseDefense": 80, + "BaseStamina": 64, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "014", + "Name": "Kakuna", + "Classification": "Cocoon Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Poison Sting" + ], + "Weight": "10.0 kg", + "Height": "0.6 m", + "Previous evolution(s)": [ + { + "Number": "013", + "Name": "Weedle" + } + ], + "Next Evolution Requirements": { + "Amount": 50, + "Family": 13, + "Name": "Weedle candies" + }, + "Next evolution(s)": [ + { + "Number": "015", + "Name": "Beedrill" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 62, + "BaseDefense": 90, + "BaseStamina": 82, + "CaptureRate": 0.2, + "FleeRate": 0.09 + }, + { + "Number": "015", + "Name": "Beedrill", + "Classification": "Poison Bee Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Poison Jab" + ], + "Weight": "29.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "013", + "Name": "Weedle" + }, + { + "Number": "014", + "Name": "Kakuna" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Sludge Bomb", + "X Scissor" + ], + "BaseAttack": 144, + "BaseDefense": 130, + "BaseStamina": 130, + "CaptureRate": 0.1, + "FleeRate": 0.06 + }, + { + "Number": "016", + "Name": "Pidgey", + "Classification": "Tiny Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Twister" + ], + "Weight": "1.8 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 12, + "Family": 16, + "Name": "Pidgey candies" + }, + "Next evolution(s)": [ + { + "Number": "017", + "Name": "Pidgeotto" + }, + { + "Number": "018", + "Name": "Pidgeot" + } + ], + "BaseAttack": 94, + "BaseDefense": 80, + "BaseStamina": 90, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "017", + "Name": "Pidgeotto", + "Classification": "Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Steel Wing", + "Wing Attack" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Twister" + ], + "Weight": "30.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "016", + "Name": "Pidgey" + } + ], + "Next Evolution Requirements": { + "Amount": 50, + "Family": 16, + "Name": "Pidgey candies" + }, + "Next evolution(s)": [ + { + "Number": "018", + "Name": "Pidgeot" + } + ], + "BaseAttack": 126, + "BaseDefense": 126, + "BaseStamina": 122, + "CaptureRate": 0.2, + "FleeRate": 0.09 + }, + { + "Number": "018", + "Name": "Pidgeot", + "Classification": "Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Steel Wing", + "Wing Attack" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Hurricane" + ], + "Weight": "39.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "016", + "Name": "Pidgey" + }, + { + "Number": "017", + "Name": "Pidgeotto" + } + ], + "BaseAttack": 170, + "BaseDefense": 166, + "BaseStamina": 166, + "CaptureRate": 0.1, + "FleeRate": 0.06 + }, + { + "Number": "019", + "Name": "Rattata", + "Classification": "Mouse Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Special Attack(s)": [ + "Body Slam", + "Dig", + "Hyper Fang" + ], + "Weight": "3.5 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 19, + "Name": "Rattata candies" + }, + "Next evolution(s)": [ + { + "Number": "020", + "Name": "Raticate" + } + ], + "BaseAttack": 92, + "BaseDefense": 60, + "BaseStamina": 86, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "020", + "Name": "Raticate", + "Classification": "Mouse Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Bite", + "Quick Attack" + ], + "Special Attack(s)": [ + "Dig", + "Hyper Beam", + "Hyper Fang" + ], + "Weight": "18.5 kg", + "Height": "0.7 m", + "Previous evolution(s)": [ + { + "Number": "019", + "Name": "Rattata" + } + ], + "BaseAttack": 146, + "BaseDefense": 110, + "BaseStamina": 150, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "021", + "Name": "Spearow", + "Classification": "Tiny Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Peck", + "Quick Attack" + ], + "Weight": "2.0 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 21, + "Name": "Spearow candies" + }, + "Next evolution(s)": [ + { + "Number": "022", + "Name": "Fearow" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Drill Peck", + "Twister" + ], + "BaseAttack": 102, + "BaseDefense": 80, + "BaseStamina": 78, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "022", + "Name": "Fearow", + "Classification": "Beak Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Peck", + "Steel Wing" + ], + "Weight": "38.0 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "021", + "Name": "Spearow" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Drill Run", + "Twister" + ], + "BaseAttack": 168, + "BaseDefense": 130, + "BaseStamina": 146, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "023", + "Name": "Ekans", + "Classification": "Snake Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Poison Sting" + ], + "Weight": "6.9 kg", + "Height": "2.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 23, + "Name": "Ekans candies" + }, + "Next evolution(s)": [ + { + "Number": "024", + "Name": "Arbok" + } + ], + "Special Attack(s)": [ + "Gunk Shot", + "Sludge Bomb", + "Wrap" + ], + "BaseAttack": 112, + "BaseDefense": 70, + "BaseStamina": 112, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "024", + "Name": "Arbok", + "Classification": "Cobra Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Bite" + ], + "Weight": "65.0 kg", + "Height": "3.5 m", + "Previous evolution(s)": [ + { + "Number": "023", + "Name": "Ekans" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Gunk Shot", + "Sludge Wave" + ], + "BaseAttack": 166, + "BaseDefense": 120, + "BaseStamina": 166, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "025", + "Name": "Pikachu", + "Classification": "Mouse Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Thunder Shock" + ], + "Weight": "6.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 25, + "Name": "Pikachu candies" + }, + "Next evolution(s)": [ + { + "Number": "026", + "Name": "Raichu" + } + ], + "Special Attack(s)": [ + "Discharge", + "Thunder", + "Thunderbolt" + ], + "BaseAttack": 124, + "BaseDefense": 70, + "BaseStamina": 108, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "026", + "Name": "Raichu", + "Classification": "Mouse Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Thunder Shock" + ], + "Weight": "30.0 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "025", + "Name": "Pikachu" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Thunder", + "Thunder Punch" + ], + "BaseAttack": 200, + "BaseDefense": 120, + "BaseStamina": 154, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "027", + "Name": "Sandshrew", + "Classification": "Mouse Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Scratch" + ], + "Weight": "12.0 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 27, + "Name": "Sandshrew candies" + }, + "Next evolution(s)": [ + { + "Number": "028", + "Name": "Sandslash" + } + ], + "Special Attack(s)": [ + "Dig", + "Rock Slide", + "Rock Tomb" + ], + "BaseAttack": 90, + "BaseDefense": 100, + "BaseStamina": 114, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "028", + "Name": "Sandslash", + "Classification": "Mouse Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Metal Claw", + "Mud Shot" + ], + "Weight": "29.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "027", + "Name": "Sandshrew" + } + ], + "Special Attack(s)": [ + "Bulldoze", + "Earthquake", + "Rock Tomb" + ], + "BaseAttack": 150, + "BaseDefense": 150, + "BaseStamina": 172, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "029", + "Name": "Nidoran F", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bite", + "Poison Sting" + ], + "Weight": "7.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 29, + "Name": "Nidoran F candies" + }, + "Next evolution(s)": [ + { + "Number": "030", + "Name": "Nidorina" + }, + { + "Number": "031", + "Name": "Nidoqueen" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Poison Fang", + "Sludge Bomb" + ], + "BaseAttack": 100, + "BaseDefense": 110, + "BaseStamina": 104, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "030", + "Name": "Nidorina", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bite", + "Poison Sting" + ], + "Weight": "20.0 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "029", + "Name": "Nidoran F" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 29, + "Name": "Nidoran F candies" + }, + "Next evolution(s)": [ + { + "Number": "031", + "Name": "Nidoqueen" + } + ], + "Special Attack(s)": [ + "Dig", + "Poison Fang", + "Sludge Bomb" + ], + "BaseAttack": 132, + "BaseDefense": 140, + "BaseStamina": 136, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "031", + "Name": "Nidoqueen", + "Classification": "Drill Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Ice", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bite", + "Poison Jab" + ], + "Weight": "60.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "029", + "Name": "Nidoran F" + }, + { + "Number": "030", + "Name": "Nidorina" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Sludge Wave", + "Stone Edge" + ], + "BaseAttack": 184, + "BaseDefense": 180, + "BaseStamina": 190, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "032", + "Name": "Nidoran M", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Peck", + "Poison Sting" + ], + "Weight": "9.0 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 32, + "Name": "Nidoran M candies" + }, + "Next evolution(s)": [ + { + "Number": "033", + "Name": "Nidorino" + }, + { + "Number": "034", + "Name": "Nidoking" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Horn Attack", + "Sludge Bomb" + ], + "BaseAttack": 110, + "BaseDefense": 92, + "BaseStamina": 94, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "033", + "Name": "Nidorino", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Poison Jab", + "Poison Sting" + ], + "Weight": "19.5 kg", + "Height": "0.9 m", + "Previous evolution(s)": [ + { + "Number": "032", + "Name": "Nidoran M" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 32, + "Name": "Nidoran M candies" + }, + "Next evolution(s)": [ + { + "Number": "034", + "Name": "Nidoking" + } + ], + "Special Attack(s)": [ + "Dig", + "Horn Attack", + "Sludge Bomb" + ], + "BaseAttack": 142, + "BaseDefense": 122, + "BaseStamina": 128, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "034", + "Name": "Nidoking", + "Classification": "Drill Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Ice", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Poison Jab" + ], + "Weight": "62.0 kg", + "Height": "1.4 m", + "Previous evolution(s)": [ + { + "Number": "032", + "Name": "Nidoran M" + }, + { + "Number": "033", + "Name": "Nidorino" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Megahorn", + "Sludge Wave" + ], + "BaseAttack": 204, + "BaseDefense": 162, + "BaseStamina": 170, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "035", + "Name": "Clefairy", + "Classification": "Fairy Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound", + "Zen Headbutt" + ], + "Weight": "7.5 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 35, + "Name": "Clefairy candies" + }, + "Next evolution(s)": [ + { + "Number": "036", + "Name": "Clefable" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Disarming Voice", + "Moonblast" + ], + "BaseAttack": 116, + "BaseDefense": 140, + "BaseStamina": 124, + "CaptureRate": 0.24, + "FleeRate": 0.1 + }, + { + "Number": "036", + "Name": "Clefable", + "Classification": "Fairy Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound", + "Zen Headbutt" + ], + "Weight": "40.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "035", + "Name": "Clefairy" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Moonblast", + "Psychic" + ], + "BaseAttack": 178, + "BaseDefense": 190, + "BaseStamina": 178, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "037", + "Name": "Vulpix", + "Classification": "Fox Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Quick Attack" + ], + "Weight": "9.9 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 37, + "Name": "Vulpix candies" + }, + "Next evolution(s)": [ + { + "Number": "038", + "Name": "Ninetales" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Flame Charge", + "Flamethrower" + ], + "BaseAttack": 106, + "BaseDefense": 76, + "BaseStamina": 118, + "CaptureRate": 0.24, + "FleeRate": 0.1 + }, + { + "Number": "038", + "Name": "Ninetales", + "Classification": "Fox Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Feint Attack" + ], + "Weight": "19.9 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "037", + "Name": "Vulpix" + } + ], + "Special Attack(s)": [ + "Fire Blast", + "Flamethrower", + "Heat Wave" + ], + "BaseAttack": 176, + "BaseDefense": 146, + "BaseStamina": 194, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "039", + "Name": "Jigglypuff", + "Classification": "Balloon Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Pound" + ], + "Weight": "5.5 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 39, + "Name": "Jigglypuff candies" + }, + "Next evolution(s)": [ + { + "Number": "039", + "Name": "Jigglypuff" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Disarming Voice", + "Play Rough" + ], + "BaseAttack": 98, + "BaseDefense": 230, + "BaseStamina": 54, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "040", + "Name": "Wigglytuff", + "Classification": "Balloon Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Pound" + ], + "Weight": "12.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "040", + "Name": "Wigglytuff" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Hyper Beam", + "Play Rough" + ], + "BaseAttack": 168, + "BaseDefense": 280, + "BaseStamina": 108, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "041", + "Name": "Zubat", + "Classification": "Bat Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Ice", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Quick Attack" + ], + "Weight": "7.5 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 41, + "Name": "Zubat candies" + }, + "Next evolution(s)": [ + { + "Number": "042", + "Name": "Golbat" + } + ], + "Special Attack(s)": [ + "Air Cutter", + "Poison Fang", + "Sludge Bomb" + ], + "BaseAttack": 88, + "BaseDefense": 80, + "BaseStamina": 90, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "042", + "Name": "Golbat", + "Classification": "Bat Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Ice", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Wing Attack" + ], + "Weight": "55.0 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "041", + "Name": "Zubat" + } + ], + "Special Attack(s)": [ + "Air Cutter", + "Ominous Wind", + "Poison Fang" + ], + "BaseAttack": 164, + "BaseDefense": 150, + "BaseStamina": 164, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "043", + "Name": "Oddish", + "Classification": "Weed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "5.4 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 43, + "Name": "Oddish candies" + }, + "Next evolution(s)": [ + { + "Number": "044", + "Name": "Gloom" + }, + { + "Number": "045", + "Name": "Vileplume" + } + ], + "Special Attack(s)": [ + "Moonblast", + "Seed Bomb", + "Sludge Bomb" + ], + "BaseAttack": 134, + "BaseDefense": 90, + "BaseStamina": 130, + "CaptureRate": 0.48, + "FleeRate": 0.15 + }, + { + "Number": "044", + "Name": "Gloom", + "Classification": "Weed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "8.6 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "043", + "Name": "Oddish" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 43, + "Name": "Oddish candies" + }, + "Next evolution(s)": [ + { + "Number": "045", + "Name": "Vileplume" + } + ], + "Special Attack(s)": [ + "Moonblast", + "Petal Blizzard", + "Sludge Bomb" + ], + "BaseAttack": 162, + "BaseDefense": 120, + "BaseStamina": 158, + "CaptureRate": 0.24, + "FleeRate": 0.07 + }, + { + "Number": "045", + "Name": "Vileplume", + "Classification": "Flower Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "18.6 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "043", + "Name": "Oddish" + }, + { + "Number": "044", + "Name": "Gloom" + } + ], + "Special Attack(s)": [ + "Moonblast", + "Petal Blizzard", + "Solar Beam" + ], + "BaseAttack": 202, + "BaseDefense": 150, + "BaseStamina": 190, + "CaptureRate": 0.12, + "FleeRate": 0.05 + }, + { + "Number": "046", + "Name": "Paras", + "Classification": "Mushroom Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Grass" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Scratch" + ], + "Weight": "5.4 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 46, + "Name": "Paras candies" + }, + "Next evolution(s)": [ + { + "Number": "047", + "Name": "Parasect" + } + ], + "Special Attack(s)": [ + "Cross Poison", + "Seed Bomb", + "X Scissor" + ], + "BaseAttack": 122, + "BaseDefense": 70, + "BaseStamina": 120, + "CaptureRate": 0.32, + "FleeRate": 0.15 + }, + { + "Number": "047", + "Name": "Parasect", + "Classification": "Mushroom Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Grass" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Fury Cutter" + ], + "Weight": "29.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "046", + "Name": "Paras" + } + ], + "Special Attack(s)": [ + "Cross Poison", + "Solar Beam", + "X Scissor" + ], + "BaseAttack": 162, + "BaseDefense": 120, + "BaseStamina": 170, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "048", + "Name": "Venonat", + "Classification": "Insect Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Confusion" + ], + "Weight": "30.0 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 48, + "Name": "Venonat candies" + }, + "Next evolution(s)": [ + { + "Number": "049", + "Name": "Venomoth" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Psybeam", + "Poison Fang", + "Shadow Ball" + ], + "BaseAttack": 108, + "BaseDefense": 120, + "BaseStamina": 118, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "049", + "Name": "Venomoth", + "Classification": "Poison Moth Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Confusion" + ], + "Weight": "12.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "048", + "Name": "Venonat" + } + ], + "Special Attack(s)": [ + "Bug Buzz", + "Poison Fang", + "Psychic" + ], + "BaseAttack": 172, + "BaseDefense": 140, + "BaseStamina": 154, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "050", + "Name": "Diglett", + "Classification": "Mole Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Scratch" + ], + "Weight": "0.8 kg", + "Height": "0.2 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 50, + "Name": "Diglett candies" + }, + "Next evolution(s)": [ + { + "Number": "051", + "Name": "Dugtrio" + } + ], + "Special Attack(s)": [ + "Dig", + "Mud Bomb", + "Rock Tomb" + ], + "BaseAttack": 108, + "BaseDefense": 20, + "BaseStamina": 86, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "051", + "Name": "Dugtrio", + "Classification": "Mole Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Sucker Punch" + ], + "Weight": "33.3 kg", + "Height": "0.7 m", + "Previous evolution(s)": [ + { + "Number": "050", + "Name": "Diglett" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Mud Bomb", + "Stone Edge" + ], + "BaseAttack": 148, + "BaseDefense": 70, + "BaseStamina": 140, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "052", + "Name": "Meowth", + "Classification": "Scratch Cat Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Bite", + "Scratch" + ], + "Weight": "4.2 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 52, + "Name": "Meowth candies" + }, + "Next evolution(s)": [ + { + "Number": "053", + "Name": "Persian" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Dark Pulse", + "Night Slash" + ], + "BaseAttack": 104, + "BaseDefense": 80, + "BaseStamina": 94, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "053", + "Name": "Persian", + "Classification": "Classy Cat Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Scratch" + ], + "Weight": "32.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "052", + "Name": "Meowth" + } + ], + "Special Attack(s)": [ + "Night Slash", + "Play Rough", + "Power Gem" + ], + "BaseAttack": 156, + "BaseDefense": 130, + "BaseStamina": 146, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "054", + "Name": "Psyduck", + "Classification": "Duck Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Water Gun", + "Zen Headbutt" + ], + "Weight": "19.6 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 54, + "Name": "Psyduck candies" + }, + "Next evolution(s)": [ + { + "Number": "055", + "Name": "Golduck" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Cross Chop", + "Psybeam" + ], + "BaseAttack": 132, + "BaseDefense": 100, + "BaseStamina": 112, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "055", + "Name": "Golduck", + "Classification": "Duck Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Confusion", + "Water Gun" + ], + "Weight": "76.6 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "054", + "Name": "Psyduck" + } + ], + "Special Attack(s)": [ + "Hydro Pump", + "Ice Beam", + "Psychic" + ], + "BaseAttack": 194, + "BaseDefense": 160, + "BaseStamina": 176, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "056", + "Name": "Mankey", + "Classification": "Pig Monkey Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Scratch" + ], + "Weight": "28.0 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 56, + "Name": "Mankey candies" + }, + "Next evolution(s)": [ + { + "Number": "057", + "Name": "Primeape" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Cross Chop", + "Low Sweep" + ], + "BaseAttack": 122, + "BaseDefense": 80, + "BaseStamina": 96, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "057", + "Name": "Primeape", + "Classification": "Pig Monkey Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Low Kick" + ], + "Weight": "32.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "056", + "Name": "Mankey" + } + ], + "Special Attack(s)": [ + "Cross Chop", + "Low Sweep", + "Night Slash" + ], + "BaseAttack": 178, + "BaseDefense": 130, + "BaseStamina": 150, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "058", + "Name": "Growlithe", + "Classification": "Puppy Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Ember" + ], + "Weight": "19.0 kg", + "Height": "0.7 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 58, + "Name": "Growlithe candies" + }, + "Next evolution(s)": [ + { + "Number": "059", + "Name": "Arcanine" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Flame Wheel", + "Flamethrower" + ], + "BaseAttack": 156, + "BaseDefense": 110, + "BaseStamina": 110, + "CaptureRate": 0.24, + "FleeRate": 0.1 + }, + { + "Number": "059", + "Name": "Arcanine", + "Classification": "Legendary Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Fire Fang" + ], + "Weight": "155.0 kg", + "Height": "1.9 m", + "Previous evolution(s)": [ + { + "Number": "058", + "Name": "Growlithe" + } + ], + "Special Attack(s)": [ + "Bulldoze", + "Fire Blast", + "Flamethrower" + ], + "BaseAttack": 230, + "BaseDefense": 180, + "BaseStamina": 180, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "060", + "Name": "Poliwag", + "Classification": "Tadpole Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "12.4 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 60, + "Name": "Poliwag candies" + }, + "Next evolution(s)": [ + { + "Number": "061", + "Name": "Poliwhirl" + }, + { + "Number": "062", + "Name": "Poliwrath" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Bubble Beam", + "Mud Bomb" + ], + "BaseAttack": 108, + "BaseDefense": 80, + "BaseStamina": 98, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "061", + "Name": "Poliwhirl", + "Classification": "Tadpole Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "20.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "060", + "Name": "Poliwag" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 60, + "Name": "Poliwag candies" + }, + "Next evolution(s)": [ + { + "Number": "062", + "Name": "Poliwrath" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Mud Bomb", + "Scald" + ], + "BaseAttack": 132, + "BaseDefense": 130, + "BaseStamina": 132, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "062", + "Name": "Poliwrath", + "Classification": "Tadpole Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Fighting" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "54.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "060", + "Name": "Poliwag" + }, + { + "Number": "061", + "Name": "Poliwhirl" + } + ], + "Special Attack(s)": [ + "Hydro Pump", + "Ice Punch", + "Submission" + ], + "BaseAttack": 180, + "BaseDefense": 180, + "BaseStamina": 202, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "063", + "Name": "Abra", + "Classification": "Psi Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Zen Headbutt" + ], + "Weight": "19.5 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 63, + "Name": "Abra candies" + }, + "Next evolution(s)": [ + { + "Number": "064", + "Name": "Kadabra" + }, + { + "Number": "065", + "Name": "Alakazam" + } + ], + "Special Attack(s)": [ + "Psyshock", + "Shadow Ball", + "Signal Beam" + ], + "BaseAttack": 110, + "BaseDefense": 50, + "BaseStamina": 76, + "CaptureRate": 0.4, + "FleeRate": 0.99 + }, + { + "Number": "064", + "Name": "Kadabra", + "Classification": "Psi Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Psycho Cut" + ], + "Weight": "56.5 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "063", + "Name": "Abra" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 63, + "Name": "Abra candies" + }, + "Next evolution(s)": [ + { + "Number": "065", + "Name": "Alakazam" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Psybeam", + "Shadow Ball" + ], + "BaseAttack": 150, + "BaseDefense": 80, + "BaseStamina": 112, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "065", + "Name": "Alakazam", + "Classification": "Psi Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Psycho Cut" + ], + "Weight": "48.0 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "063", + "Name": "Abra" + }, + { + "Number": "064", + "Name": "Kadabra" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Psychic", + "Shadow Ball" + ], + "BaseAttack": 186, + "BaseDefense": 110, + "BaseStamina": 152, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "066", + "Name": "Machop", + "Classification": "Superpower Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Low Kick" + ], + "Weight": "19.5 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 66, + "Name": "Machop candies" + }, + "Next evolution(s)": [ + { + "Number": "067", + "Name": "Machoke" + }, + { + "Number": "068", + "Name": "Machamp" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Cross Chop", + "Low Sweep" + ], + "BaseAttack": 118, + "BaseDefense": 140, + "BaseStamina": 96, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "067", + "Name": "Machoke", + "Classification": "Superpower Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Low Kick" + ], + "Weight": "70.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "066", + "Name": "Machop" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 66, + "Name": "Machop candies" + }, + "Next evolution(s)": [ + { + "Number": "068", + "Name": "Machamp" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Cross Chop", + "Submission" + ], + "BaseAttack": 154, + "BaseDefense": 160, + "BaseStamina": 144, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "068", + "Name": "Machamp", + "Classification": "Superpower Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Bullet Punch", + "Karate Chop" + ], + "Weight": "130.0 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "066", + "Name": "Machop" + }, + { + "Number": "067", + "Name": "Machoke" + } + ], + "Special Attack(s)": [ + "Cross Chop", + "Stone Edge", + "Submission" + ], + "BaseAttack": 198, + "BaseDefense": 180, + "BaseStamina": 180, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "069", + "Name": "Bellsprout", + "Classification": "Flower Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Vine Whip" + ], + "Weight": "4.0 kg", + "Height": "0.7 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 69, + "Name": "Bellsprout candies" + }, + "Next evolution(s)": [ + { + "Number": "070", + "Name": "Weepinbell" + }, + { + "Number": "071", + "Name": "Victreebel" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Sludge Bomb", + "Wrap" + ], + "BaseAttack": 158, + "BaseDefense": 100, + "BaseStamina": 78, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "070", + "Name": "Weepinbell", + "Classification": "Flycatcher Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "6.4 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "069", + "Name": "Bellsprout" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 69, + "Name": "Bellsprout candies" + }, + "Next evolution(s)": [ + { + "Number": "071", + "Name": "Victreebel" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Seed Bomb", + "Sludge Bomb" + ], + "BaseAttack": 190, + "BaseDefense": 130, + "BaseStamina": 110, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "071", + "Name": "Victreebel", + "Classification": "Flycatcher Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "15.5 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "069", + "Name": "Bellsprout" + }, + { + "Number": "070", + "Name": "Weepinbell" + } + ], + "Special Attack(s)": [ + "Leaf Blade", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 222, + "BaseDefense": 160, + "BaseStamina": 152, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "072", + "Name": "Tentacool", + "Classification": "Jellyfish Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Electric", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bubble", + "Poison Sting" + ], + "Weight": "45.5 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 72, + "Name": "Tentacool candies" + }, + "Next evolution(s)": [ + { + "Number": "073", + "Name": "Tentacruel" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Water Pulse", + "Wrap" + ], + "BaseAttack": 106, + "BaseDefense": 80, + "BaseStamina": 136, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "073", + "Name": "Tentacruel", + "Classification": "Jellyfish Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Electric", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Poison Jab" + ], + "Weight": "55.0 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "072", + "Name": "Tentacool" + } + ], + "Special Attack(s)": [ + "Blizzard", + "Hydro Pump", + "Sludge Wave" + ], + "BaseAttack": 170, + "BaseDefense": 160, + "BaseStamina": 196, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "074", + "Name": "Geodude", + "Classification": "Rock Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Rock Throw", + "Tackle" + ], + "Weight": "20.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 74, + "Name": "Geodude candies" + }, + "Next evolution(s)": [ + { + "Number": "075", + "Name": "Graveler" + }, + { + "Number": "076", + "Name": "Golem" + } + ], + "Special Attack(s)": [ + "Dig", + "Rock Slide", + "Rock Tomb" + ], + "BaseAttack": 106, + "BaseDefense": 80, + "BaseStamina": 118, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "075", + "Name": "Graveler", + "Classification": "Rock Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Rock Throw" + ], + "Weight": "105.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "074", + "Name": "Geodude" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 74, + "Name": "Geodude candies" + }, + "Next evolution(s)": [ + { + "Number": "076", + "Name": "Golem" + } + ], + "Special Attack(s)": [ + "Dig", + "Rock Slide", + "Stone Edge" + ], + "BaseAttack": 142, + "BaseDefense": 110, + "BaseStamina": 156, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "076", + "Name": "Golem", + "Classification": "Megaton Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Rock Throw" + ], + "Weight": "300.0 kg", + "Height": "1.4 m", + "Previous evolution(s)": [ + { + "Number": "074", + "Name": "Geodude" + }, + { + "Number": "075", + "Name": "Graveler" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Earthquake", + "Stone Edge" + ], + "BaseAttack": 176, + "BaseDefense": 160, + "BaseStamina": 198, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "077", + "Name": "Ponyta", + "Classification": "Fire Horse Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Tackle" + ], + "Weight": "30.0 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 77, + "Name": "Ponyta candies" + }, + "Next evolution(s)": [ + { + "Number": "078", + "Name": "Rapidash" + } + ], + "Special Attack(s)": [ + "Fire Blast", + "Flame Charge", + "Flame Wheel" + ], + "BaseAttack": 168, + "BaseDefense": 100, + "BaseStamina": 138, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "078", + "Name": "Rapidash", + "Classification": "Fire Horse Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Low Kick" + ], + "Weight": "95.0 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "077", + "Name": "Ponyta" + } + ], + "Special Attack(s)": [ + "Drill Run", + "Fire Blast", + "Heat Wave" + ], + "BaseAttack": 200, + "BaseDefense": 130, + "BaseStamina": 170, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "079", + "Name": "Slowpoke", + "Classification": "Dopey Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Water Gun" + ], + "Weight": "36.0 kg", + "Height": "1.2 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 79, + "Name": "Slowpoke candies" + }, + "Next evolution(s)": [ + { + "Number": "080", + "Name": "Slowbro" + } + ], + "Special Attack(s)": [ + "Psychic", + "Psyshock", + "Water Pulse" + ], + "BaseAttack": 110, + "BaseDefense": 180, + "BaseStamina": 110, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "080", + "Name": "Slowbro", + "Classification": "Hermit Crab Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Water Gun" + ], + "Weight": "78.5 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "079", + "Name": "Slowpoke" + } + ], + "Special Attack(s)": [ + "Ice Beam", + "Psychic", + "Water Pulse" + ], + "BaseAttack": 184, + "BaseDefense": 190, + "BaseStamina": 198, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "081", + "Name": "Magnemite", + "Classification": "Magnet Pokemon", + "Type I": [ + "Electric" + ], + "Type II": [ + "Steel" + ], + "Weaknesses": [ + "Fire", + "Water", + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Thunder Shock" + ], + "Weight": "6.0 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 81, + "Name": "Magnemite candies" + }, + "Next evolution(s)": [ + { + "Number": "082", + "Name": "Magneton" + } + ], + "Special Attack(s)": [ + "Discharge", + "Magnet Bomb", + "Thunderbolt" + ], + "BaseAttack": 128, + "BaseDefense": 50, + "BaseStamina": 138, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "082", + "Name": "Magneton", + "Classification": "Magnet Pokemon", + "Type I": [ + "Electric" + ], + "Type II": [ + "Steel" + ], + "Weaknesses": [ + "Fire", + "Water", + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Thunder Shock" + ], + "Weight": "60.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "081", + "Name": "Magnemite" + } + ], + "Special Attack(s)": [ + "Discharge", + "Flash Cannon", + "Magnet Bomb" + ], + "BaseAttack": 186, + "BaseDefense": 100, + "BaseStamina": 180, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "083", + "Name": "Farfetch'd", + "Classification": "Wild Duck Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Cut", + "Fury Cutter" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Leaf Blade" + ], + "Weight": "15.0 kg", + "Height": "0.8 m", + "BaseAttack": 138, + "BaseDefense": 104, + "BaseStamina": 132, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "084", + "Name": "Doduo", + "Classification": "Twin Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Peck", + "Quick Attack" + ], + "Weight": "39.2 kg", + "Height": "1.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 84, + "Name": "Doduo candies" + }, + "Next evolution(s)": [ + { + "Number": "085", + "Name": "Dodrio" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Drill Peck", + "Swift" + ], + "BaseAttack": 126, + "BaseDefense": 70, + "BaseStamina": 96, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "085", + "Name": "Dodrio", + "Classification": "Triple Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Steel Wing" + ], + "Weight": "85.2 kg", + "Height": "1.8 m", + "Previous evolution(s)": [ + { + "Number": "084", + "Name": "Doduo" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Drill Peck" + ], + "BaseAttack": 182, + "BaseDefense": 120, + "BaseStamina": 150, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "086", + "Name": "Seel", + "Classification": "Sea Lion Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Ice Shard", + "Water Gun" + ], + "Weight": "90.0 kg", + "Height": "1.1 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 86, + "Name": "Seel candies" + }, + "Next evolution(s)": [ + { + "Number": "087", + "Name": "Dewgong" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Aqua Tail", + "Icy Wind" + ], + "BaseAttack": 104, + "BaseDefense": 130, + "BaseStamina": 138, + "CaptureRate": 0.4, + "FleeRate": 0.09 + }, + { + "Number": "087", + "Name": "Dewgong", + "Classification": "Sea Lion Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Ice" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Rock" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Ice Shard" + ], + "Weight": "120.0 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "086", + "Name": "Seel" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Blizzard", + "Icy Wind" + ], + "BaseAttack": 156, + "BaseDefense": 180, + "BaseStamina": 192, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "088", + "Name": "Grimer", + "Classification": "Sludge Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Mud Slap" + ], + "Weight": "30.0 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 88, + "Name": "Grimer candies" + }, + "Next evolution(s)": [ + { + "Number": "089", + "Name": "Muk" + } + ], + "Special Attack(s)": [ + "Mud Bomb", + "Sludge", + "Sludge Bomb" + ], + "BaseAttack": 124, + "BaseDefense": 160, + "BaseStamina": 110, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "089", + "Name": "Muk", + "Classification": "Sludge Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Poison Jab" + ], + "Weight": "30.0 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "088", + "Name": "Grimer" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Gunk Shot", + "Sludge Wave" + ], + "BaseAttack": 180, + "BaseDefense": 210, + "BaseStamina": 188, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "090", + "Name": "Shellder", + "Classification": "Bivalve Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Ice Shard", + "Tackle" + ], + "Weight": "4.0 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 90, + "Name": "Shellder candies" + }, + "Next evolution(s)": [ + { + "Number": "091", + "Name": "Cloyster" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Icy Wind", + "Water Pulse" + ], + "BaseAttack": 120, + "BaseDefense": 60, + "BaseStamina": 112, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "091", + "Name": "Cloyster", + "Classification": "Bivalve Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Ice" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Rock" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Ice Shard" + ], + "Weight": "132.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "090", + "Name": "Shellder" + } + ], + "Special Attack(s)": [ + "Blizzard", + "Hydro Pump", + "Icy Wind" + ], + "BaseAttack": 196, + "BaseDefense": 100, + "BaseStamina": 196, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "092", + "Name": "Gastly", + "Classification": "Gas Pokemon", + "Type I": [ + "Ghost" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Lick", + "Sucker Punch" + ], + "Weight": "0.1 kg", + "Height": "1.3 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 92, + "Name": "Gastly candies" + }, + "Next evolution(s)": [ + { + "Number": "093", + "Name": "Haunter" + }, + { + "Number": "094", + "Name": "Gengar" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Ominous Wind", + "Sludge Bomb" + ], + "BaseAttack": 136, + "BaseDefense": 60, + "BaseStamina": 82, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "093", + "Name": "Haunter", + "Classification": "Gas Pokemon", + "Type I": [ + "Ghost" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Lick", + "Shadow Claw" + ], + "Weight": "0.1 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "092", + "Name": "Gastly" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 92, + "Name": "Gastly candies" + }, + "Next evolution(s)": [ + { + "Number": "094", + "Name": "Gengar" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Shadow Ball", + "Sludge Bomb" + ], + "BaseAttack": 172, + "BaseDefense": 90, + "BaseStamina": 118, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "094", + "Name": "Gengar", + "Classification": "Shadow Pokemon", + "Type I": [ + "Ghost" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Shadow Claw", + "Sucker Punch" + ], + "Weight": "40.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "092", + "Name": "Gastly" + }, + { + "Number": "093", + "Name": "Haunter" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Shadow Ball", + "Sludge Wave" + ], + "BaseAttack": 204, + "BaseDefense": 120, + "BaseStamina": 156, + "CaptureRate": 0.08, + "FleeRate": 0.05 + }, + { + "Number": "095", + "Name": "Onix", + "Classification": "Rock Snake Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Rock Throw", + "Tackle" + ], + "Weight": "210.0 kg", + "Height": "8.8 m", + "Special Attack(s)": [ + "Iron Head", + "Rock Slide", + "Stone Edge" + ], + "BaseAttack": 90, + "BaseDefense": 70, + "BaseStamina": 186, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "096", + "Name": "Drowzee", + "Classification": "Hypnosis Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Pound" + ], + "Weight": "32.4 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 96, + "Name": "Drowzee candies" + }, + "Next evolution(s)": [ + { + "Number": "097", + "Name": "Hypno" + } + ], + "Special Attack(s)": [ + "Psybeam", + "Psychic", + "Psyshock" + ], + "BaseAttack": 104, + "BaseDefense": 120, + "BaseStamina": 140, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "097", + "Name": "Hypno", + "Classification": "Hypnosis Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Zen Headbutt" + ], + "Weight": "75.6 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "096", + "Name": "Drowzee" + } + ], + "Special Attack(s)": [ + "Psychic", + "Psyshock", + "Shadow Ball" + ], + "BaseAttack": 162, + "BaseDefense": 170, + "BaseStamina": 196, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "098", + "Name": "Krabby", + "Classification": "River Crab Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "6.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 98, + "Name": "Krabby candies" + }, + "Next evolution(s)": [ + { + "Number": "099", + "Name": "Kingler" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Vice Grip", + "Water Pulse" + ], + "BaseAttack": 116, + "BaseDefense": 60, + "BaseStamina": 110, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "099", + "Name": "Kingler", + "Classification": "Pincer Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Metal Claw", + "Mud Shot" + ], + "Weight": "60.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "098", + "Name": "Krabby" + } + ], + "Special Attack(s)": [ + "Vice Grip", + "Water Pulse", + "X Scissor" + ], + "BaseAttack": 178, + "BaseDefense": 110, + "BaseStamina": 168, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "100", + "Name": "Voltorb", + "Classification": "Ball Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Tackle" + ], + "Weight": "10.4 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 100, + "Name": "Voltorb candies" + }, + "Next evolution(s)": [ + { + "Number": "101", + "Name": "Electrode" + } + ], + "Special Attack(s)": [ + "Discharge", + "Signal Beam", + "Thunderbolt" + ], + "BaseAttack": 102, + "BaseDefense": 80, + "BaseStamina": 124, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "101", + "Name": "Electrode", + "Classification": "Ball Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Tackle" + ], + "Weight": "66.6 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "100", + "Name": "Voltorb" + } + ], + "Special Attack(s)": [ + "Discharge", + "Hyper Beam", + "Thunderbolt" + ], + "BaseAttack": 150, + "BaseDefense": 120, + "BaseStamina": 174, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "102", + "Name": "Exeggcute", + "Classification": "Egg Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion" + ], + "Weight": "2.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 102, + "Name": "Exeggcute candies" + }, + "Next evolution(s)": [ + { + "Number": "103", + "Name": "Exeggutor" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Psychic", + "Seed Bomb" + ], + "BaseAttack": 110, + "BaseDefense": 120, + "BaseStamina": 132, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "103", + "Name": "Exeggutor", + "Classification": "Coconut Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Zen Headbutt" + ], + "Weight": "120.0 kg", + "Height": "2.0 m", + "Previous evolution(s)": [ + { + "Number": "102", + "Name": "Exeggcute" + } + ], + "Special Attack(s)": [ + "Psychic", + "Seed Bomb", + "Solar Beam" + ], + "BaseAttack": 232, + "BaseDefense": 190, + "BaseStamina": 164, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "104", + "Name": "Cubone", + "Classification": "Lonely Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "6.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 104, + "Name": "Cubone candies" + }, + "Next evolution(s)": [ + { + "Number": "105", + "Name": "Marowak" + } + ], + "Special Attack(s)": [ + "Bone Club", + "Bulldoze", + "Dig" + ], + "BaseAttack": 102, + "BaseDefense": 100, + "BaseStamina": 150, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "105", + "Name": "Marowak", + "Classification": "Bone Keeper Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "45.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "104", + "Name": "Cubone" + } + ], + "Special Attack(s)": [ + "Bone Club", + "Dig", + "Earthquake" + ], + "BaseAttack": 140, + "BaseDefense": 120, + "BaseStamina": 202, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "106", + "Name": "Hitmonlee", + "Classification": "Kicking Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Low Kick", + "Rock Smash" + ], + "Weight": "49.8 kg", + "Height": "1.5 m", + "Next evolution(s)": [ + { + "Number": "107", + "Name": "Hitmonchan" + } + ], + "Special Attack(s)": [ + "Low Sweep", + "Stomp", + "Stone Edge" + ], + "BaseAttack": 148, + "BaseDefense": 100, + "BaseStamina": 172, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "107", + "Name": "Hitmonchan", + "Classification": "Punching Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Bullet Punch", + "Rock Smash" + ], + "Weight": "50.2 kg", + "Height": "1.4 m", + "Previous evolution(s)": [ + { + "Number": "106", + "Name": "Hitmonlee" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Fire Punch", + "Ice Punch", + "Thunder Punch" + ], + "BaseAttack": 138, + "BaseDefense": 100, + "BaseStamina": 204, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "108", + "Name": "Lickitung", + "Classification": "Licking Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Lick", + "Zen Headbutt" + ], + "Weight": "65.5 kg", + "Height": "1.2 m", + "Special Attack(s)": [ + "Hyper Beam", + "Power Whip", + "Stomp" + ], + "BaseAttack": 126, + "BaseDefense": 180, + "BaseStamina": 160, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "109", + "Name": "Koffing", + "Classification": "Poison Gas Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Tackle" + ], + "Weight": "1.0 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 109, + "Name": "Koffing candies" + }, + "Next evolution(s)": [ + { + "Number": "110", + "Name": "Weezing" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Sludge", + "Sludge Bomb" + ], + "BaseAttack": 136, + "BaseDefense": 80, + "BaseStamina": 142, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "110", + "Name": "Weezing", + "Classification": "Poison Gas Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Tackle" + ], + "Weight": "9.5 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "109", + "Name": "Koffing" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Shadow Ball", + "Sludge Bomb" + ], + "BaseAttack": 190, + "BaseDefense": 130, + "BaseStamina": 198, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "111", + "Name": "Rhyhorn", + "Classification": "Spikes Pokemon", + "Type I": [ + "Ground" + ], + "Type II": [ + "Rock" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "115.0 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 111, + "Name": "Rhyhorn candies" + }, + "Next evolution(s)": [ + { + "Number": "112", + "Name": "Rhydon" + } + ], + "Special Attack(s)": [ + "Bulldoze", + "Horn Attack", + "Stomp" + ], + "BaseAttack": 110, + "BaseDefense": 160, + "BaseStamina": 116, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "112", + "Name": "Rhydon", + "Classification": "Drill Pokemon", + "Type I": [ + "Ground" + ], + "Type II": [ + "Rock" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "120.0 kg", + "Height": "1.9 m", + "Previous evolution(s)": [ + { + "Number": "111", + "Name": "Rhyhorn" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Megahorn", + "Stone Edge" + ], + "BaseAttack": 166, + "BaseDefense": 210, + "BaseStamina": 160, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "113", + "Name": "Chansey", + "Classification": "Egg Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound", + "Zen Headbutt" + ], + "Weight": "34.6 kg", + "Height": "1.1 m", + "Special Attack(s)": [ + "Dazzling Gleam", + "Psybeam", + "Psychic" + ], + "BaseAttack": 40, + "BaseDefense": 500, + "BaseStamina": 60, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "114", + "Name": "Tangela", + "Classification": "Vine Pokemon", + "Type I": [ + "Grass" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug" + ], + "Fast Attack(s)": [ + "Vine Whip" + ], + "Weight": "35.0 kg", + "Height": "1.0 m", + "Special Attack(s)": [ + "Power Whip", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 164, + "BaseDefense": 130, + "BaseStamina": 152, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "115", + "Name": "Kangaskhan", + "Classification": "Parent Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Low Kick", + "Mud Slap" + ], + "Weight": "80.0 kg", + "Height": "2.2 m", + "Special Attack(s)": [ + "Brick Break", + "Earthquake", + "Stomp" + ], + "BaseAttack": 142, + "BaseDefense": 210, + "BaseStamina": 178, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "116", + "Name": "Horsea", + "Classification": "Dragon Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Water Gun" + ], + "Weight": "8.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 116, + "Name": "Horsea candies" + }, + "Next evolution(s)": [ + { + "Number": "117", + "Name": "Seadra" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Dragon Pulse", + "Flash Cannon" + ], + "BaseAttack": 122, + "BaseDefense": 60, + "BaseStamina": 100, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "117", + "Name": "Seadra", + "Classification": "Dragon Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Dragon Breath", + "Water Gun" + ], + "Weight": "25.0 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "116", + "Name": "Horsea" + } + ], + "Special Attack(s)": [ + "Blizzard", + "Dragon Pulse", + "Hydro Pump" + ], + "BaseAttack": 176, + "BaseDefense": 110, + "BaseStamina": 150, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "118", + "Name": "Goldeen", + "Classification": "Goldfish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Peck" + ], + "Weight": "15.0 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 118, + "Name": "Goldeen candies" + }, + "Next evolution(s)": [ + { + "Number": "119", + "Name": "Seaking" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Horn Attack", + "Water Pulse" + ], + "BaseAttack": 112, + "BaseDefense": 90, + "BaseStamina": 126, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "119", + "Name": "Seaking", + "Classification": "Goldfish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Peck", + "Poison Jab" + ], + "Weight": "39.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "118", + "Name": "Goldeen" + } + ], + "Special Attack(s)": [ + "Drill Run", + "Icy Wind", + "Megahorn" + ], + "BaseAttack": 172, + "BaseDefense": 160, + "BaseStamina": 160, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "120", + "Name": "Staryu", + "Classification": "Starshape Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Water Gun" + ], + "Weight": "34.5 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 120, + "Name": "Staryu candies" + }, + "Next evolution(s)": [ + { + "Number": "120", + "Name": "Staryu" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Power Gem", + "Swift" + ], + "BaseAttack": 130, + "BaseDefense": 60, + "BaseStamina": 128, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "121", + "Name": "Starmie", + "Classification": "Mysterious Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Water Gun" + ], + "Weight": "80.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "121", + "Name": "Starmie" + } + ], + "Special Attack(s)": [ + "Hydro Pump", + "Power Gem", + "Psybeam" + ], + "BaseAttack": 194, + "BaseDefense": 120, + "BaseStamina": 192, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "122", + "Name": "Mr. Mime", + "Classification": "Barrier Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Zen Headbutt" + ], + "Weight": "54.5 kg", + "Height": "1.3 m", + "Special Attack(s)": [ + "Psybeam", + "Psychic", + "Shadow Ball" + ], + "BaseAttack": 154, + "BaseDefense": 80, + "BaseStamina": 196, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "123", + "Name": "Scyther", + "Classification": "Mantis Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Fire", + "Electric", + "Ice", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Steel Wing" + ], + "Weight": "56.0 kg", + "Height": "1.5 m", + "Special Attack(s)": [ + "Bug Buzz", + "Night Slash", + "X Scissor" + ], + "BaseAttack": 176, + "BaseDefense": 140, + "BaseStamina": 180, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "124", + "Name": "Jynx", + "Classification": "Humanshape Pokemon", + "Type I": [ + "Ice" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Fire", + "Bug", + "Rock", + "Ghost", + "Dark", + "Steel" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Pound" + ], + "Weight": "40.6 kg", + "Height": "1.4 m", + "Special Attack(s)": [ + "Draining Kiss", + "Ice Punch", + "Psyshock" + ], + "BaseAttack": 172, + "BaseDefense": 130, + "BaseStamina": 134, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "125", + "Name": "Electabuzz", + "Classification": "Electric Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Low Kick", + "Thunder Shock" + ], + "Weight": "30.0 kg", + "Height": "1.1 m", + "Special Attack(s)": [ + "Thunder", + "Thunder Punch", + "Thunderbolt" + ], + "BaseAttack": 198, + "BaseDefense": 130, + "BaseStamina": 160, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "126", + "Name": "Magmar", + "Classification": "Spitfire Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Karate Chop" + ], + "Weight": "44.5 kg", + "Height": "1.3 m", + "Special Attack(s)": [ + "Fire Blast", + "Fire Punch", + "Flamethrower" + ], + "BaseAttack": 214, + "BaseDefense": 130, + "BaseStamina": 158, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "127", + "Name": "Pinsir", + "Classification": "Stagbeetle Pokemon", + "Type I": [ + "Bug" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Rock Smash" + ], + "Weight": "55.0 kg", + "Height": "1.5 m", + "Special Attack(s)": [ + "Submission", + "Vice Grip", + "X Scissor" + ], + "BaseAttack": 184, + "BaseDefense": 130, + "BaseStamina": 186, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "128", + "Name": "Tauros", + "Classification": "Wild Bull Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Tackle", + "Zen Headbutt" + ], + "Weight": "88.4 kg", + "Height": "1.4 m", + "Special Attack(s)": [ + "Earthquake", + "Horn Attack", + "Iron Head" + ], + "BaseAttack": 148, + "BaseDefense": 150, + "BaseStamina": 184, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "129", + "Name": "Magikarp", + "Classification": "Fish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Splash" + ], + "Weight": "10.0 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 400, + "Family": 129, + "Name": "Magikarp candies" + }, + "Next evolution(s)": [ + { + "Number": "130", + "Name": "Gyarados" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 42, + "BaseDefense": 40, + "BaseStamina": 84, + "CaptureRate": 0.56, + "FleeRate": 0.15 + }, + { + "Number": "130", + "Name": "Gyarados", + "Classification": "Atrocious Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Dragon Breath" + ], + "Weight": "235.0 kg", + "Height": "6.5 m", + "Previous evolution(s)": [ + { + "Number": "129", + "Name": "Magikarp" + } + ], + "Special Attack(s)": [ + "Dragon Pulse", + "Hydro Pump", + "Twister" + ], + "BaseAttack": 192, + "BaseDefense": 190, + "BaseStamina": 196, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "131", + "Name": "Lapras", + "Classification": "Transport Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Ice" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Rock" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Ice Shard" + ], + "Weight": "220.0 kg", + "Height": "2.5 m", + "Special Attack(s)": [ + "Blizzard", + "Dragon Pulse", + "Ice Beam" + ], + "BaseAttack": 186, + "BaseDefense": 260, + "BaseStamina": 190, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "132", + "Name": "Ditto", + "Classification": "Transform Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound" + ], + "Special Attack(s)": [ + "Struggle" + ], + "Weight": "4.0 kg", + "Height": "0.3 m", + "BaseAttack": 110, + "BaseDefense": 96, + "BaseStamina": 110, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "133", + "Name": "Eevee", + "Classification": "Evolution Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Weight": "6.5 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 133, + "Name": "Eevee candies" + }, + "Next evolution(s)": [ + { + "Number": "134", + "Name": "Vaporeon" + }, + { + "Number": "135", + "Name": "Jolteon" + }, + { + "Number": "136", + "Name": "Flareon" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Dig", + "Swift" + ], + "BaseAttack": 114, + "BaseDefense": 110, + "BaseStamina": 128, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "134", + "Name": "Vaporeon", + "Classification": "Bubble Jet Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Water Gun" + ], + "Weight": "29.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "133", + "Name": "Eevee" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Hydro Pump", + "Water Pulse" + ], + "BaseAttack": 186, + "BaseDefense": 260, + "BaseStamina": 168, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "135", + "Name": "Jolteon", + "Classification": "Lightning Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Thunder Shock" + ], + "Weight": "24.5 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "133", + "Name": "Eevee" + } + ], + "Special Attack(s)": [ + "Discharge", + "Thunder", + "Thunderbolt" + ], + "BaseAttack": 192, + "BaseDefense": 130, + "BaseStamina": 174, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "136", + "Name": "Flareon", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember" + ], + "Weight": "25.0 kg", + "Height": "0.9 m", + "Previous evolution(s)": [ + { + "Number": "133", + "Name": "Eevee" + } + ], + "Special Attack(s)": [ + "Fire Blast", + "Flamethrower", + "Heat Wave" + ], + "BaseAttack": 238, + "BaseDefense": 130, + "BaseStamina": 178, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "137", + "Name": "Porygon", + "Classification": "Virtual Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Weight": "36.5 kg", + "Height": "0.8 m", + "Special Attack(s)": [ + "Discharge", + "Psybeam", + "Signal Beam" + ], + "BaseAttack": 156, + "BaseDefense": 130, + "BaseStamina": 158, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "138", + "Name": "Omanyte", + "Classification": "Spiral Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Water Gun" + ], + "Weight": "7.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 138, + "Name": "Omanyte candies" + }, + "Next evolution(s)": [ + { + "Number": "139", + "Name": "Omastar" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Brine", + "Rock Tomb" + ], + "BaseAttack": 132, + "BaseDefense": 70, + "BaseStamina": 160, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "139", + "Name": "Omastar", + "Classification": "Spiral Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Rock Throw", + "Water Gun" + ], + "Weight": "35.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "138", + "Name": "Omanyte" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Hydro Pump", + "Rock Slide" + ], + "BaseAttack": 180, + "BaseDefense": 140, + "BaseStamina": 202, + "CaptureRate": 0.12, + "FleeRate": 0.05 + }, + { + "Number": "140", + "Name": "Kabuto", + "Classification": "Shellfish Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Scratch" + ], + "Weight": "11.5 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 140, + "Name": "Kabuto candies" + }, + "Next evolution(s)": [ + { + "Number": "141", + "Name": "Kabutops" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Aqua Jet", + "Rock Tomb" + ], + "BaseAttack": 148, + "BaseDefense": 60, + "BaseStamina": 142, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "141", + "Name": "Kabutops", + "Classification": "Shellfish Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Mud Shot" + ], + "Weight": "40.5 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "140", + "Name": "Kabuto" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Stone Edge", + "Water Pulse" + ], + "BaseAttack": 190, + "BaseDefense": 120, + "BaseStamina": 190, + "CaptureRate": 0.12, + "FleeRate": 0.05 + }, + { + "Number": "142", + "Name": "Aerodactyl", + "Classification": "Fossil Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Water", + "Electric", + "Ice", + "Rock", + "Steel" + ], + "Fast Attack(s)": [ + "Bite", + "Steel Wing" + ], + "Weight": "59.0 kg", + "Height": "1.8 m", + "Special Attack(s)": [ + "Ancient Power", + "Hyper Beam", + "Iron Head" + ], + "BaseAttack": 182, + "BaseDefense": 160, + "BaseStamina": 162, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "143", + "Name": "Snorlax", + "Classification": "Sleeping Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Lick", + "Zen Headbutt" + ], + "Weight": "460.0 kg", + "Height": "2.1 m", + "Special Attack(s)": [ + "Body Slam", + "Earthquake", + "Hyper Beam" + ], + "BaseAttack": 180, + "BaseDefense": 320, + "BaseStamina": 180, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "144", + "Name": "Articuno", + "Classification": "Freeze Pokemon", + "Type I": [ + "Ice" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Fire", + "Electric", + "Rock", + "Steel" + ], + "Fast Attack(s)": [ + "Frost Breath" + ], + "Special Attack(s)": [ + "Blizzard", + "Ice Beam", + "Icy Wind" + ], + "Weight": "55.4 kg", + "Height": "1.7 m", + "BaseAttack": 198, + "BaseDefense": 180, + "BaseStamina": 242, + "CaptureRate": 0.0, + "FleeRate": 0.1 + }, + { + "Number": "145", + "Name": "Zapdos", + "Classification": "Electric Pokemon", + "Type I": [ + "Electric" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Ice", + "Rock" + ], + "Fast Attack(s)": [ + "Thunder Shock" + ], + "Special Attack(s)": [ + "Discharge", + "Thunder", + "Thunderbolt" + ], + "Weight": "52.6 kg", + "Height": "1.6 m", + "BaseAttack": 232, + "BaseDefense": 180, + "BaseStamina": 194, + "CaptureRate": 0.0, + "FleeRate": 0.1 + }, + { + "Number": "146", + "Name": "Moltres", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Water", + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Ember" + ], + "Special Attack(s)": [ + "Fire Blast", + "Flamethrower", + "Heat Wave" + ], + "Weight": "60.0 kg", + "Height": "2.0 m", + "BaseAttack": 242, + "BaseDefense": 180, + "BaseStamina": 194, + "CaptureRate": 0.0, + "FleeRate": 0.1 + }, + { + "Number": "147", + "Name": "Dratini", + "Classification": "Dragon Pokemon", + "Type I": [ + "Dragon" + ], + "Weaknesses": [ + "Ice", + "Dragon", + "Fairy" + ], + "Fast Attack(s)": [ + "Dragon Breath" + ], + "Weight": "3.3 kg", + "Height": "1.8 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 147, + "Name": "Dratini candies" + }, + "Special Attack(s)": [ + "Aqua Tail", + "Twister", + "Wrap" + ], + "BaseAttack": 128, + "BaseDefense": 82, + "BaseStamina": 110, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "148", + "Name": "Dragonair", + "Classification": "Dragon Pokemon", + "Type I": [ + "Dragon" + ], + "Weaknesses": [ + "Ice", + "Dragon", + "Fairy" + ], + "Fast Attack(s)": [ + "Dragon Breath" + ], + "Weight": "16.5 kg", + "Height": "4.0 m", + "Previous evolution(s)": [ + { + "Number": "147", + "Name": "Dratini" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 147, + "Name": "Dratini candies" + }, + "Next evolution(s)": [ + { + "Number": "149", + "Name": "Dragonite" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Dragon Pulse", + "Wrap" + ], + "BaseAttack": 170, + "BaseDefense": 122, + "BaseStamina": 152, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "149", + "Name": "Dragonite", + "Classification": "Dragon Pokemon", + "Type I": [ + "Dragon" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Ice", + "Rock", + "Dragon", + "Fairy" + ], + "Fast Attack(s)": [ + "Dragon Breath", + "Steel Wing" + ], + "Weight": "210.0 kg", + "Height": "2.2 m", + "Previous evolution(s)": [ + { + "Number": "147", + "Name": "Dratini" + }, + { + "Number": "148", + "Name": "Dragonair" + } + ], + "Special Attack(s)": [ + "Dragon Claw", + "Dragon Pulse", + "Hyper Beam" + ], + "BaseAttack": 250, + "BaseDefense": 182, + "BaseStamina": 212, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "150", + "Name": "Mewtwo", + "Classification": "Genetic Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Psycho Cut" + ], + "Special Attack(s)": [ + "Hyper Beam", + "Psychic", + "Shadow Ball" + ], + "Weight": "122.0 kg", + "Height": "2.0 m", + "BaseAttack": 284, + "BaseDefense": 212, + "BaseStamina": 202, + "CaptureRate": 0.0, + "FleeRate": 0.1 + }, + { + "Number": "151", + "Name": "Mew", + "Classification": "New Species Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Pound" + ], + "Special Attack(s)": [ + "Dragon Pulse", + "Earthquake", + "Fire Blast", + "Hurricane", + "Hyper Beam", + "Moonblast", + "Psychic", + "Solar Beam", + "Thunder" + ], + "Weight": "4.0 kg", + "Height": "0.4 m", + "BaseAttack": 220, + "BaseDefense": 200, + "BaseStamina": 220, + "CaptureRate": 0.0, + "FleeRate": 0.1 + } +] From 2b31f93d2293e35f7cdb90b359707b5e62f4d897 Mon Sep 17 00:00:00 2001 From: Quantra Date: Tue, 9 Aug 2016 19:36:27 +0100 Subject: [PATCH 118/202] Made paths to .json files absolute so pokecli.py can be called from CRON (#3157) * Made paths to .json files absolute so pokecli.py can be called from CRON * made file paths abs in inventory fixed incorrect dict reference, changed to .get() as felt this was intended --- pokecli.py | 3 +- pokemongo_bot/__init__.py | 33 ++++++++++--------- pokemongo_bot/base_dir.py | 4 +++ .../cell_workers/catch_visible_pokemon.py | 4 ++- .../cell_workers/move_to_map_pokemon.py | 5 +-- pokemongo_bot/cell_workers/recycle_items.py | 3 +- .../cell_workers/transfer_pokemon.py | 4 ++- pokemongo_bot/inventory.py | 7 ++-- 8 files changed, 38 insertions(+), 25 deletions(-) create mode 100644 pokemongo_bot/base_dir.py diff --git a/pokecli.py b/pokecli.py index 55afa55399..6811da7696 100644 --- a/pokecli.py +++ b/pokecli.py @@ -39,6 +39,7 @@ from geopy.exc import GeocoderQuotaExceeded from pokemongo_bot import PokemonGoBot, TreeConfigBuilder +from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.health_record import BotEvent from pokemongo_bot.plugin_loader import PluginLoader @@ -162,7 +163,7 @@ def report_summary(bot): def init_config(): parser = argparse.ArgumentParser() - config_file = "configs/config.json" + config_file = os.path.join(_base_dir, 'configs', 'config.json') web_dir = "web" # If config file exists, load variables from json diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index dd4de22551..133bb4631e 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -28,6 +28,7 @@ from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler from pokemongo_bot.socketio_server.runner import SocketIoRunner from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl +from pokemongo_bot.base_dir import _base_dir from worker_result import WorkerResult from tree_config_builder import ConfigException, MismatchTaskApiVersion, TreeConfigBuilder from inventory import init_inventory @@ -57,9 +58,9 @@ def __init__(self, config): self.config = config self.fort_timeouts = dict() self.pokemon_list = json.load( - open(os.path.join('data', 'pokemon.json')) + open(os.path.join(_base_dir, 'data', 'pokemon.json')) ) - self.item_list = json.load(open(os.path.join('data', 'items.json'))) + self.item_list = json.load(open(os.path.join(_base_dir, 'data', 'items.json'))) self.metrics = Metrics(self) self.latest_inventory = None self.cell = None @@ -109,12 +110,12 @@ def _setup_event_system(self): self.event_manager.event_report() sys.exit(1) - # Registering event: - # self.event_manager.register_event("location", parameters=['lat', 'lng']) - # - # Emitting event should be enough to add logging and send websocket - # message: : - # self.event_manager.emit('location', 'level'='info', data={'lat': 1, 'lng':1}), + # Registering event: + # self.event_manager.register_event("location", parameters=['lat', 'lng']) + # + # Emitting event should be enough to add logging and send websocket + # message: : + # self.event_manager.emit('location', 'level'='info', data={'lat': 1, 'lng':1}), def _register_events(self): self.event_manager.register_event( @@ -498,12 +499,12 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): location = self.position[0:2] cells = self.find_close_cells(*location) - user_data_cells = "data/cells-%s.json" % self.config.username + user_data_cells = os.path.join(_base_dir, 'data', 'cells-%s.json' % self.config.username) with open(user_data_cells, 'w') as outfile: json.dump(cells, outfile) user_web_location = os.path.join( - 'web', 'location-%s.json' % self.config.username + _base_dir, 'web', 'location-%s.json' % self.config.username ) # alt is unused atm but makes using *location easier try: @@ -518,7 +519,7 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): self.logger.info('[x] Error while opening location file: %s' % e) user_data_lastlocation = os.path.join( - 'data', 'last-location-%s.json' % self.config.username + _base_dir, 'data', 'last-location-%s.json' % self.config.username ) try: with open(user_data_lastlocation, 'w') as outfile: @@ -790,7 +791,7 @@ def current_inventory(self): inventory_dict = inventory_req['responses']['GET_INVENTORY'][ 'inventory_delta']['inventory_items'] - user_web_inventory = 'web/inventory-%s.json' % self.config.username + user_web_inventory = os.path.join(_base_dir, 'web', 'inventory-%s.json' % self.config.username) with open(user_web_inventory, 'w') as outfile: json.dump(inventory_dict, outfile) @@ -901,8 +902,8 @@ def _set_starting_position(self): level='debug', formatted='Loading cached location...' ) - with open('data/last-location-%s.json' % - self.config.username) as f: + with open(os.path.join(_base_dir, 'data', 'last-location-%s.json' % + self.config.username)) as f: location_json = json.load(f) location = ( location_json['lat'], @@ -1051,8 +1052,8 @@ def has_space_for_loot(self): def get_forts(self, order_by_distance=False): forts = [fort - for fort in self.cell['forts'] - if 'latitude' in fort and 'type' in fort] + for fort in self.cell['forts'] + if 'latitude' in fort and 'type' in fort] if order_by_distance: forts.sort(key=lambda x: distance( diff --git a/pokemongo_bot/base_dir.py b/pokemongo_bot/base_dir.py new file mode 100644 index 0000000000..83978c964a --- /dev/null +++ b/pokemongo_bot/base_dir.py @@ -0,0 +1,4 @@ +import os + + +_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py index 654c2467b3..0203459c28 100644 --- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -1,9 +1,11 @@ import json +import os from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker from utils import distance from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.base_dir import _base_dir class CatchVisiblePokemon(BaseTask): @@ -27,7 +29,7 @@ def work(self): key= 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) + user_web_catchable = os.path.join(_base_dir, '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) diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 2cfd45d14b..7ff0eb1cf4 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -54,6 +54,7 @@ import json import base64 import requests +from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.cell_workers.utils import distance, format_dist, format_time from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult @@ -84,7 +85,7 @@ def initialize(self): self.caught = [] self.min_ball = self.config.get('min_ball', 1) - data_file = 'data/map-caught-{}.json'.format(self.bot.config.username) + data_file = os.path.join(_base_dir, 'map-caught-{}.json'.format(self.bot.config.username)) if os.path.isfile(data_file): self.caught = json.load( open(data_file) @@ -222,7 +223,7 @@ def snipe(self, pokemon): return WorkerResult.SUCCESS def dump_caught_pokemon(self): - user_data_map_caught = 'data/map-caught-{}.json'.format(self.bot.config.username) + user_data_map_caught = os.path.join(_base_dir, 'data', 'map-caught-{}.json'.format(self.bot.config.username)) with open(user_data_map_caught, 'w') as outfile: json.dump(self.caught, outfile) diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 673b373fba..15c8f7931a 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -1,5 +1,6 @@ import json import os +from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.base_task import BaseTask from pokemongo_bot.tree_config_builder import ConfigException @@ -13,7 +14,7 @@ def initialize(self): self._validate_item_filter() def _validate_item_filter(self): - item_list = json.load(open(os.path.join('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(): if config_item_name not in item_list.viewvalues(): if config_item_name not in item_list: diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index ebc197ef24..88548e75b2 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -1,5 +1,7 @@ import json +import os +from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.human_behaviour import action_delay from pokemongo_bot.base_task import BaseTask @@ -83,7 +85,7 @@ def _release_pokemon_get_groups(self): inventory_dict = inventory_req['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] - user_web_inventory = 'web/inventory-%s.json' % (self.bot.config.username) + 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_dict, outfile) diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index c74a85296f..1fc208d235 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1,5 +1,6 @@ import json import os +from pokemongo_bot.base_dir import _base_dir ''' Helper class for updating/retrieving Inventory data @@ -42,7 +43,7 @@ def refresh(self, inventory): self._data = self.retrieve_data(inventory) def get(self, id): - return self._data(id) + return self._data.get(id) def all(self): return list(self._data.values()) @@ -96,7 +97,7 @@ def captured(self, pokemon_id): class Items(_BaseInventoryComponent): TYPE = 'item' ID_FIELD = 'item_id' - STATIC_DATA_FILE = os.path.join('data', 'items.json') + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'items.json') def count_for(self, item_id): return self._data[item_id]['count'] @@ -105,7 +106,7 @@ def count_for(self, item_id): class Pokemons(_BaseInventoryComponent): TYPE = 'pokemon_data' ID_FIELD = 'id' - STATIC_DATA_FILE = os.path.join('data', 'pokemon.json') + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'pokemon.json') def parse(self, item): if 'is_egg' in item: From ceb5db71cb1d00ebdd62b2a4f93cc4b18a44ccdb Mon Sep 17 00:00:00 2001 From: Amal Samally Date: Tue, 9 Aug 2016 23:08:00 +0400 Subject: [PATCH 119/202] Add fast & charged moves data from #2117 (originally by @iananass) (#3336) Data for pokemon quick & slow attacks --- data/charged_moves.json | 92 +++++++++++++++++++++++++++++++++++++++++ data/fast_moves.json | 41 ++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 data/charged_moves.json create mode 100644 data/fast_moves.json diff --git a/data/charged_moves.json b/data/charged_moves.json new file mode 100644 index 0000000000..3d487b7201 --- /dev/null +++ b/data/charged_moves.json @@ -0,0 +1,92 @@ +[{"id":32,"name":"Stone Edge","type":"Rock","damage":80,"duration":3100,"energy":100,"dps":25.8}, +{"id":28,"name":"Cross Chop","type":"Fighting","damage":60,"duration":2000,"energy":100,"dps":30.0}, +{"id":83,"name":"Dragon Claw","type":"Dragon","damage":35,"duration":1500,"energy":50,"dps":23.33}, +{"id":40,"name":"Blizzard","type":"Ice","damage":100,"duration":3900,"energy":100,"dps":25.64}, +{"id":131,"name":"Body Slam","type":"Normal","damage":40,"duration":1560,"energy":50,"dps":25.64}, +{"id":22,"name":"Megahorn","type":"Bug","damage":80,"duration":3200,"energy":100,"dps":25.0}, +{"id":122,"name":"Hurricane","type":"Flying","damage":80,"duration":3200,"energy":100,"dps":25.0}, +{"id":116,"name":"Solar Beam","type":"Grass","damage":120,"duration":4900,"energy":100,"dps":24.48}, +{"id":103,"name":"Fire Blast","type":"Fire","damage":100,"duration":4100,"energy":100,"dps":24.39}, +{"id":14,"name":"Hyper Beam","type":"Normal","damage":120,"duration":5000,"energy":100,"dps":24.0}, +{"id":31,"name":"Earthquake","type":"Ground","damage":100,"duration":4200,"energy":100,"dps":23.8}, +{"id":118,"name":"Power Whip","type":"Grass","damage":70,"duration":2800,"energy":100,"dps":25.0}, +{"id":107,"name":"Hydro Pump","type":"Water","damage":90,"duration":3800,"energy":100,"dps":23.68}, +{"id":117,"name":"Leaf Blade","type":"Grass","damage":55,"duration":2800,"energy":50,"dps":19.64}, +{"id":78,"name":"Thunder","type":"Electric","damage":100,"duration":4300,"energy":100,"dps":23.25}, +{"id":123,"name":"Brick Break","type":"Fighting","damage":30,"duration":1600,"energy":33,"dps":18.75}, +{"id":92,"name":"Gunk Shot","type":"Poison","damage":65,"duration":3000,"energy":100,"dps":21.66}, +{"id":90,"name":"Sludge Bomb","type":"Poison","damage":55,"duration":2600,"energy":50,"dps":21.15}, +{"id":42,"name":"Heat Wave","type":"Fire","damage":80,"duration":3800,"energy":100,"dps":21.05}, +{"id":87,"name":"Moonblast","type":"Fairy","damage":85,"duration":4100,"energy":100,"dps":20.73}, +{"id":91,"name":"Sludge Wave","type":"Poison","damage":70,"duration":3400,"energy":100,"dps":20.58}, +{"id":79,"name":"Thunderbolt","type":"Electric","damage":55,"duration":2700,"energy":50,"dps":20.37}, +{"id":47,"name":"Petal Blizzard","type":"Grass","damage":65,"duration":3200,"energy":50,"dps":20.31}, +{"id":89,"name":"Cross Poison","type":"Poison","damage":25,"duration":1500,"energy":25,"dps":16.66}, +{"id":108,"name":"Psychic","type":"Psychic","damage":55,"duration":2800,"energy":50,"dps":19.64}, +{"id":58,"name":"Aqua Tail","type":"Water","damage":45,"duration":2350,"energy":50,"dps":19.14}, +{"id":24,"name":"Flamethrower","type":"Fire","damage":55,"duration":2900,"energy":50,"dps":18.96}, +{"id":88,"name":"Play Rough","type":"Fairy","damage":55,"duration":2900,"energy":50,"dps":18.96}, +{"id":82,"name":"Dragon Pulse","type":"Dragon","damage":65,"duration":3600,"energy":50,"dps":18.05}, +{"id":39,"name":"Ice Beam","type":"Ice","damage":65,"duration":3650,"energy":50,"dps":17.8}, +{"id":49,"name":"Bug Buzz","type":"Bug","damage":75,"duration":4250,"energy":50,"dps":17.64}, +{"id":46,"name":"Drill Run","type":"Ground","damage":50,"duration":3400,"energy":33,"dps":14.7}, +{"id":59,"name":"Seed Bomb","type":"Grass","damage":40,"duration":2400,"energy":33,"dps":16.66}, +{"id":77,"name":"Thunder Punch","type":"Electric","damage":40,"duration":2400,"energy":33,"dps":16.66}, +{"id":100,"name":"X Scissor","type":"Bug","damage":35,"duration":2100,"energy":33,"dps":16.66}, +{"id":129,"name":"Hyper Fang","type":"Normal","damage":35,"duration":2100,"energy":33,"dps":16.66}, +{"id":64,"name":"Rock Slide","type":"Rock","damage":50,"duration":3200,"energy":33,"dps":15.62}, +{"id":94,"name":"Bone Club","type":"Ground","damage":25,"duration":1600,"energy":25,"dps":15.62}, +{"id":36,"name":"Flash Cannon","type":"Steel","damage":60,"duration":3900,"energy":33,"dps":15.38}, +{"id":74,"name":"Iron Head","type":"Steel","damage":30,"duration":2000,"energy":33,"dps":15.0}, +{"id":38,"name":"Drill Peck","type":"Flying","damage":40,"duration":2700,"energy":33,"dps":14.81}, +{"id":60,"name":"Psyshock","type":"Psychic","damage":40,"duration":2700,"energy":33,"dps":14.81}, +{"id":70,"name":"Shadow Ball","type":"Ghost","damage":45,"duration":3080,"energy":33,"dps":14.61}, +{"id":99,"name":"Signal Beam","type":"Bug","damage":45,"duration":3100,"energy":33,"dps":14.51}, +{"id":115,"name":"Fire Punch","type":"Fire","damage":40,"duration":2800,"energy":33,"dps":14.28}, +{"id":54,"name":"Submission","type":"Fighting","damage":30,"duration":2100,"energy":33,"dps":14.28}, +{"id":102,"name":"Flame Burst","type":"Fire","damage":30,"duration":2100,"energy":25,"dps":14.28}, +{"id":127,"name":"Stomp","type":"Normal","damage":30,"duration":2100,"energy":25,"dps":14.28}, +{"id":35,"name":"Discharge","type":"Electric","damage":35,"duration":2500,"energy":33,"dps":14.0}, +{"id":65,"name":"Power Gem","type":"Rock","damage":40,"duration":2900,"energy":33,"dps":13.79}, +{"id":106,"name":"Scald","type":"Water","damage":55,"duration":4000,"energy":33,"dps":13.75}, +{"id":109,"name":"Psystrike","type":"Psychic","damage":70,"duration":5100,"energy":100,"dps":13.72}, +{"id":56,"name":"Low Sweep","type":"Fighting","damage":30,"duration":2250,"energy":25,"dps":13.33}, +{"id":51,"name":"Night Slash","type":"Dark","damage":30,"duration":2700,"energy":25,"dps":11.11}, +{"id":86,"name":"Dazzling Gleam","type":"Fairy","damage":55,"duration":4200,"energy":33,"dps":13.09}, +{"id":16,"name":"Dark Pulse","type":"Dark","damage":45,"duration":3500,"energy":33,"dps":12.85}, +{"id":33,"name":"Ice Punch","type":"Ice","damage":45,"duration":3500,"energy":33,"dps":12.85}, +{"id":26,"name":"Dig","type":"Ground","damage":70,"duration":5800,"energy":33,"dps":12.06}, +{"id":20,"name":"Vice Grip","type":"Normal","damage":25,"duration":2100,"energy":20,"dps":11.9}, +{"id":18,"name":"Sludge","type":"Poison","damage":30,"duration":2600,"energy":25,"dps":11.53}, +{"id":96,"name":"Mud Bomb","type":"Ground","damage":30,"duration":2600,"energy":25,"dps":11.53}, +{"id":126,"name":"Horn Attack","type":"Normal","damage":25,"duration":2200,"energy":25,"dps":11.36}, +{"id":121,"name":"Air Cutter","type":"Flying","damage":30,"duration":3300,"energy":25,"dps":9.09}, +{"id":132,"name":"Rest","type":"Normal","damage":35,"duration":3100,"energy":33,"dps":11.29}, +{"id":72,"name":"Magnet Bomb","type":"Steel","damage":30,"duration":2800,"energy":25,"dps":10.71}, +{"id":57,"name":"Aqua Jet","type":"Water","damage":25,"duration":2350,"energy":20,"dps":10.63}, +{"id":105,"name":"Water Pulse","type":"Water","damage":35,"duration":3300,"energy":25,"dps":10.6}, +{"id":30,"name":"Psybeam","type":"Psychic","damage":40,"duration":3800,"energy":25,"dps":10.52}, +{"id":63,"name":"Rock Tomb","type":"Rock","damage":30,"duration":3400,"energy":25,"dps":8.82}, +{"id":50,"name":"Poison Fang","type":"Poison","damage":25,"duration":2400,"energy":20,"dps":10.41}, +{"id":104,"name":"Brine","type":"Water","damage":25,"duration":2400,"energy":25,"dps":10.41}, +{"id":45,"name":"Aerial Ace","type":"Flying","damage":30,"duration":2900,"energy":25,"dps":10.34}, +{"id":53,"name":"Bubble Beam","type":"Water","damage":30,"duration":2900,"energy":25,"dps":10.34}, +{"id":95,"name":"Bulldoze","type":"Ground","damage":35,"duration":3400,"energy":25,"dps":10.29}, +{"id":125,"name":"Swift","type":"Normal","damage":30,"duration":3000,"energy":25,"dps":10.0}, +{"id":62,"name":"Ancient Power","type":"Rock","damage":35,"duration":3600,"energy":25,"dps":9.72}, +{"id":114,"name":"Giga Drain","type":"Grass","damage":35,"duration":3600,"energy":33,"dps":9.72}, +{"id":69,"name":"Ominous Wind","type":"Ghost","damage":30,"duration":3100,"energy":25,"dps":9.67}, +{"id":67,"name":"Shadow Punch","type":"Ghost","damage":20,"duration":2100,"energy":25,"dps":9.52}, +{"id":80,"name":"Twister","type":"Dragon","damage":25,"duration":2700,"energy":20,"dps":9.25}, +{"id":85,"name":"Draining Kiss","type":"Fairy","damage":25,"duration":2800,"energy":20,"dps":8.92}, +{"id":21,"name":"Flame Wheel","type":"Fire","damage":40,"duration":4600,"energy":25,"dps":8.69}, +{"id":133,"name":"Struggle","type":"Normal","damage":15,"duration":1695,"energy":20,"dps":8.84}, +{"id":101,"name":"Flame Charge","type":"Fire","damage":25,"duration":3100,"energy":20,"dps":8.06}, +{"id":34,"name":"Heart Stamp","type":"Psychic","damage":20,"duration":2550,"energy":25,"dps":7.84}, +{"id":75,"name":"Parabolic Charge","type":"Electric","damage":15,"duration":2100,"energy":20,"dps":7.14}, +{"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":3700,"energy":20,"dps":6.75}, +{"id":111,"name":"Icy Wind","type":"Ice","damage":25,"duration":3800,"energy":20,"dps":6.57}, +{"id":84,"name":"Disarming Voice","type":"Fairy","damage":25,"duration":3900,"energy":20,"dps":6.41}, +{"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":4000,"energy":20,"dps":6.25}, +{"id":66,"name":"Shadow Sneak","type":"Ghost","damage":15,"duration":3100,"energy":20,"dps":4.83}, +{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] \ No newline at end of file diff --git a/data/fast_moves.json b/data/fast_moves.json new file mode 100644 index 0000000000..ec16d2cc8a --- /dev/null +++ b/data/fast_moves.json @@ -0,0 +1,41 @@ +[{"id":222,"name":"Pound","type":"Normal","damage":7,"duration":540,"energy":7,"dps":12.96}, +{"id":228,"name":"Metal Claw","type":"Steel","damage":8,"duration":630,"energy":7,"dps":12.69}, +{"id":226,"name":"Psycho Cut","type":"Psychic","damage":7,"duration":570,"energy":7,"dps":12.28}, +{"id":210,"name":"Wing Attack","type":"Flying","damage":9,"duration":750,"energy":7,"dps":12.0}, +{"id":202,"name":"Bite","type":"Dark","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":204,"name":"Dragon Breath","type":"Dragon","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":220,"name":"Scratch","type":"Normal","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":230,"name":"Water Gun","type":"Water","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":240,"name":"Fire Fang","type":"Fire","damage":10,"duration":840,"energy":4,"dps":11.9}, +{"id":213,"name":"Shadow Claw","type":"Ghost","damage":11,"duration":950,"energy":7,"dps":11.57}, +{"id":238,"name":"Feint Attack","type":"Dark","damage":12,"duration":1040,"energy":7,"dps":11.53}, +{"id":224,"name":"Poison Jab","type":"Poison","damage":12,"duration":1050,"energy":7,"dps":11.42}, +{"id":234,"name":"Zen Headbutt","type":"Psychic","damage":12,"duration":1050,"energy":4,"dps":11.42}, +{"id":239,"name":"Steel Wing","type":"Steel","damage":15,"duration":1330,"energy":4,"dps":11.27}, +{"id":201,"name":"Bug Bite","type":"Bug","damage":5,"duration":450,"energy":7,"dps":11.11}, +{"id":218,"name":"Frost Breath","type":"Ice","damage":9,"duration":810,"energy":7,"dps":11.11}, +{"id":233,"name":"Mud Slap","type":"Ground","damage":15,"duration":1350,"energy":9,"dps":11.11}, +{"id":216,"name":"Mud Shot","type":"Ground","damage":6,"duration":550,"energy":7,"dps":10.9}, +{"id":221,"name":"Tackle","type":"Normal","damage":12,"duration":1100,"energy":7,"dps":10.9}, +{"id":237,"name":"Bubble","type":"Water","damage":25,"duration":2300,"energy":15,"dps":10.86}, +{"id":214,"name":"Vine Whip","type":"Grass","damage":7,"duration":650,"energy":7,"dps":10.76}, +{"id":217,"name":"Ice Shard","type":"Ice","damage":15,"duration":1400,"energy":7,"dps":10.71}, +{"id":241,"name":"Rock Smash","type":"Fighting","damage":15,"duration":1410,"energy":7,"dps":10.63}, +{"id":223,"name":"Cut","type":"Normal","damage":12,"duration":1130,"energy":7,"dps":10.61}, +{"id":236,"name":"Poison Sting","type":"Poison","damage":6,"duration":575,"energy":4,"dps":10.43}, +{"id":215,"name":"Razor Leaf","type":"Grass","damage":15,"duration":1450,"energy":7,"dps":10.34}, +{"id":212,"name":"Lick","type":"Ghost","damage":5,"duration":500,"energy":7,"dps":10.0}, +{"id":206,"name":"Spark","type":"Electric","damage":7,"duration":700,"energy":4,"dps":10.0}, +{"id":203,"name":"Sucker Punch","type":"Dark","damage":7,"duration":700,"energy":4,"dps":10.0}, +{"id":235,"name":"Confusion","type":"Psychic","damage":15,"duration":1510,"energy":7,"dps":9.93}, +{"id":225,"name":"Acid","type":"Poison","damage":10,"duration":1050,"energy":7,"dps":9.52}, +{"id":209,"name":"Ember","type":"Fire","damage":10,"duration":1050,"energy":7,"dps":9.52}, +{"id":227,"name":"Rock Throw","type":"Rock","damage":12,"duration":1360,"energy":7,"dps":8.82}, +{"id":211,"name":"Peck","type":"Flying","damage":10,"duration":1150,"energy":10,"dps":8.69}, +{"id":207,"name":"Low Kick","type":"Fighting","damage":5,"duration":600,"energy":7,"dps":8.33}, +{"id":205,"name":"Thunder Shock","type":"Electric","damage":5,"duration":600,"energy":7,"dps":8.33}, +{"id":229,"name":"Bullet Punch","type":"Steel","damage":10,"duration":1200,"energy":7,"dps":8.33}, +{"id":219,"name":"Quick Attack","type":"Normal","damage":10,"duration":1330,"energy":7,"dps":7.51}, +{"id":200,"name":"Fury Cutter","type":"Bug","damage":3,"duration":400,"energy":12,"dps":7.5}, +{"id":208,"name":"Karate Chop","type":"Fighting","damage":6,"duration":800,"energy":7,"dps":7.5}, +{"id":231,"name":"Splash","type":"Water","damage":0,"duration":1230,"energy":7,"dps":0.0}] \ No newline at end of file From bb2c9522586f82e70011bbc63f0326c765741e1e Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Tue, 9 Aug 2016 12:18:45 -0700 Subject: [PATCH 120/202] Upgrade pgoapi to the b4bf0e089dfe09903f8dda37dae56910e01f94cc commit(latest for now). (#3337) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76b1d15a7b..617f48a76c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy==1.11.0 networkx==1.11 --e git+https://github.com/keyphact/pgoapi.git@a2755eb42dfe49e359798d2f4defefc97fb8163d#egg=pgoapi +-e git+https://github.com/keyphact/pgoapi.git@b4bf0e089dfe09903f8dda37dae56910e01f94cc#egg=pgoapi geopy==1.11.0 protobuf==3.0.0b4 requests==2.10.0 From 9172937009600cae642842f1574e93e7873ed50a Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Tue, 9 Aug 2016 12:19:39 -0700 Subject: [PATCH 121/202] =?UTF-8?q?Revert=20"Upgrade=20pgoapi=20to=20the?= =?UTF-8?q?=20b4bf0e089dfe09903f8dda37dae56910e01f94cc=20commit=E2=80=A6"?= =?UTF-8?q?=20(#3340)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 617f48a76c..76b1d15a7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy==1.11.0 networkx==1.11 --e git+https://github.com/keyphact/pgoapi.git@b4bf0e089dfe09903f8dda37dae56910e01f94cc#egg=pgoapi +-e git+https://github.com/keyphact/pgoapi.git@a2755eb42dfe49e359798d2f4defefc97fb8163d#egg=pgoapi geopy==1.11.0 protobuf==3.0.0b4 requests==2.10.0 From 3519c317fd242b79aeff857dbf4b2532455773d2 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Tue, 9 Aug 2016 12:26:16 -0700 Subject: [PATCH 122/202] Added map_path configuration for move_to_map. (#3339) --- configs/config.json.map.example | 1 + pokemongo_bot/cell_workers/move_to_map_pokemon.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/configs/config.json.map.example b/configs/config.json.map.example index bb6878f5ac..56a006450c 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -66,6 +66,7 @@ "snipe_high_prio_threshold": 400, "update_map": true, "mode": "priority", + "map_path": "raw_data", "catch": { "==========Legendaries==========": 0, "Aerodactyl": 1000, diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 7ff0eb1cf4..efd058ca96 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -84,6 +84,7 @@ def initialize(self): self.unit = self.bot.config.distance_unit self.caught = [] self.min_ball = self.config.get('min_ball', 1) + self.map_path = self.config.get('map_path', 'raw_data') data_file = os.path.join(_base_dir, 'map-caught-{}.json'.format(self.bot.config.username)) if os.path.isfile(data_file): @@ -93,7 +94,7 @@ def initialize(self): def get_pokemon_from_map(self): try: - req = requests.get('{}/raw_data?gyms=false&scanned=false'.format(self.config['address'])) + req = requests.get('{}/{}?gyms=false&scanned=false'.format(self.config['address'], self.map_path)) except requests.exceptions.ConnectionError: self._emit_failure('Could not get Pokemon data from PokemonGo-Map: ' '{}. Is it running?'.format( From b94d3695e454b1020775e4b717fe397e18646fce Mon Sep 17 00:00:00 2001 From: spalacio Date: Tue, 9 Aug 2016 23:32:33 +0200 Subject: [PATCH 123/202] Log stats on terminal (#3312) * added _log_on_terminal function * Added logic to toggle terminal logging functionality from config file * Added possibility to disable title changes, refactor code * Refactor tuples * Refactor ifs to clearer syntax --- .../cell_workers/update_title_stats.py | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index 0a6e3c592d..bc40ed82e8 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -18,10 +18,22 @@ class UpdateTitleStats(BaseTask): "type": "UpdateTitleStats", "config": { "min_interval": 10, - "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"] + "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"], } } + You can set a logging on terminal mode like this: + + Example logging on console (and disabling title change): + { + "type": "UpdateTitleStats", + "config": { + "min_interval": 10, + "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"], + "terminal_log": true, + "terminal_title": false + } + } Available stats : - login : The account login (from the credentials). - username : The trainer name (asked at first in-game connection). @@ -53,8 +65,6 @@ class UpdateTitleStats(BaseTask): """ SUPPORTED_TASK_API_VERSION = 1 - DEFAULT_MIN_INTERVAL = 10 - DEFAULT_DISPLAYED_STATS = [] def __init__(self, bot, config): """ @@ -67,12 +77,14 @@ def __init__(self, bot, config): super(UpdateTitleStats, self).__init__(bot, config) self.next_update = None - self.min_interval = self.DEFAULT_MIN_INTERVAL - self.displayed_stats = self.DEFAULT_DISPLAYED_STATS - self.bot.event_manager.register_event('update_title', parameters=('title')) + self.min_interval = int(self.config.get('min_interval', 120)) + self.displayed_stats = self.config.get('stats', []) + self.terminal_log = self.config.get('terminal_log', False) + self.terminal_title = self.config.get('terminal_title', True) - self._process_config() + self.bot.event_manager.register_event('update_title', parameters=('title',)) + self.bot.event_manager.register_event('log_stats',parameters=('title',)) def initialize(self): pass @@ -89,7 +101,12 @@ def work(self): # If title is empty, it couldn't be generated. if not title: return WorkerResult.SUCCESS - self._update_title(title, _platform) + + if self.terminal_title: + self._update_title(title, _platform) + + if self.terminal_log: + self._log_on_terminal(title) return WorkerResult.SUCCESS def _should_display(self): @@ -100,6 +117,16 @@ def _should_display(self): """ return self.next_update is None or datetime.now() >= self.next_update + def _log_on_terminal(self, title): + self.emit_event( + 'log_stats', + formatted="{title}", + data={ + 'title': title + } + ) + self.next_update = datetime.now() + timedelta(seconds=self.min_interval) + def _update_title(self, title, platform): """ Updates the window title using different methods, according to the given platform @@ -119,7 +146,7 @@ def _update_title(self, title, platform): 'title': title } ) - + if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) stdout.flush() @@ -133,14 +160,6 @@ def _update_title(self, title, platform): self.next_update = datetime.now() + timedelta(seconds=self.min_interval) - def _process_config(self): - """ - Fetches the configuration for this worker and stores the values internally. - :return: Nothing. - :rtype: None - """ - self.min_interval = int(self.config.get('min_interval', self.DEFAULT_MIN_INTERVAL)) - self.displayed_stats = self.config.get('stats', self.DEFAULT_DISPLAYED_STATS) def _get_stats_title(self, player_stats): """ From 2127bef8d7464f1c7fcd0b61dc104f090139e4ec Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Tue, 9 Aug 2016 23:56:29 +0200 Subject: [PATCH 124/202] changes to improve event system based on new web ui devs requests --- pokemongo_bot/__init__.py | 25 +++++++++++++++---- .../cell_workers/pokemon_catch_worker.py | 18 +++++++++++-- pokemongo_bot/socketio_server/app.py | 6 ++--- pokemongo_bot/step_walker.py | 11 ++++++++ pokemongo_bot/websocket_remote_control.py | 9 +++++-- 5 files changed, 57 insertions(+), 12 deletions(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 133bb4631e..7e9e3eaf89 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -234,6 +234,10 @@ def _register_events(self): 'cp', 'iv', 'iv_display', + 'encounter_id', + 'latitude', + 'longitude', + 'pokemon_id' ) ) self.event_manager.register_event('no_pokeballs') @@ -268,7 +272,13 @@ def _register_events(self): ) self.event_manager.register_event( 'pokemon_vanished', - parameters=('pokemon',) + parameters=( + 'pokemon', + 'encounter_id', + 'latitude', + 'longitude', + 'pokemon_id' + ) ) self.event_manager.register_event('pokemon_not_in_range') self.event_manager.register_event('pokemon_inventory_full') @@ -276,7 +286,11 @@ def _register_events(self): 'pokemon_caught', parameters=( 'pokemon', - 'cp', 'iv', 'iv_display', 'exp' + 'cp', 'iv', 'iv_display', 'exp', + 'encounter_id', + 'latitude', + 'longitude', + 'pokemon_id' ) ) self.event_manager.register_event( @@ -988,9 +1002,10 @@ def heartbeat(self): pass def update_web_location_worker(self): - while True: - self.web_update_queue.get() - self.update_web_location() + pass + # while True: + # self.web_update_queue.get() + # self.update_web_location() def get_inventory_count(self, what): response_dict = self.get_inventory() diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index d551a68632..345fcabc7e 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -98,6 +98,10 @@ def work(self, response_dict=None): 'cp': pokemon.cp, 'iv': pokemon.iv, 'iv_display': pokemon.iv_display, + 'encounter_id': self.pokemon['encounter_id'], + 'latitude': self.pokemon['latitude'], + 'longitude': self.pokemon['longitude'], + 'pokemon_id': pokemon.num } ) @@ -370,7 +374,13 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): self.emit_event( 'pokemon_vanished', formatted='{pokemon} vanished!', - data={'pokemon': pokemon.name} + data={ + 'pokemon': pokemon.name, + 'encounter_id': self.pokemon['encounter_id'], + 'latitude': self.pokemon['latitude'], + 'longitude': self.pokemon['longitude'], + 'pokemon_id': pokemon.num + } ) if self._pct(catch_rate_by_ball[current_ball]) == 100: self.bot.softban = True @@ -386,7 +396,11 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): 'cp': pokemon.cp, 'iv': pokemon.iv, 'iv_display': pokemon.iv_display, - 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) + 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']), + 'encounter_id': self.pokemon['encounter_id'], + 'latitude': self.pokemon['latitude'], + 'longitude': self.pokemon['longitude'], + 'pokemon_id': pokemon.num } ) diff --git a/pokemongo_bot/socketio_server/app.py b/pokemongo_bot/socketio_server/app.py index 09c237f910..7f3c5040d2 100644 --- a/pokemongo_bot/socketio_server/app.py +++ b/pokemongo_bot/socketio_server/app.py @@ -26,7 +26,7 @@ def request_reply(sid, response): @sio.on('bot:broadcast') def bot_broadcast(sid, env): - event = env.pop('event') - account = env.pop('account') + event = env['event'] + account = env['account'] event_name = "{}:{}".format(event, account) - sio.emit(event_name, data=env['data']) + sio.emit(event_name, data=env) diff --git a/pokemongo_bot/step_walker.py b/pokemongo_bot/step_walker.py index f6c2cbfe96..7727dc6a0c 100644 --- a/pokemongo_bot/step_walker.py +++ b/pokemongo_bot/step_walker.py @@ -55,6 +55,17 @@ def step(self): cLng = self.initLng + scaledDLng + random_lat_long_delta() self.api.set_position(cLat, cLng, 0) + self.bot.event_manager.emit( + 'position_update', + sender=self, + level='debug', + data={ + 'current_position': (cLat, cLng), + 'last_position': (self.initLat, self.initLng), + 'distance': '', + 'distance_unit': '' + } + ) self.bot.heartbeat() sleep(1) # sleep one second plus a random delta diff --git a/pokemongo_bot/websocket_remote_control.py b/pokemongo_bot/websocket_remote_control.py index c4e15362b6..14645e6572 100644 --- a/pokemongo_bot/websocket_remote_control.py +++ b/pokemongo_bot/websocket_remote_control.py @@ -42,11 +42,16 @@ def on_remote_command(self, command): command_handler() def get_player_info(self): - player_info = self.bot.get_inventory()['responses']['GET_INVENTORY'] + request = self.api.create_request() + request.get_player() + request.get_inventory() + response_dict = request.call() + inventory = response_dict['responses']['GET_INVENTORY'] + player_info = response_dict['responses']['GET_PLAYER'] self.sio.emit( 'bot:send_reply', { - 'result': player_info, + 'result': {'inventory': inventory, 'player': player_info}, 'command': 'get_player_info', 'account': self.bot.config.username } From c556b48aa9a34d5d93e1827784461b42b09c2900 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Wed, 10 Aug 2016 01:34:40 +0200 Subject: [PATCH 125/202] typo :D --- pokemongo_bot/websocket_remote_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/websocket_remote_control.py b/pokemongo_bot/websocket_remote_control.py index 14645e6572..023db22ff4 100644 --- a/pokemongo_bot/websocket_remote_control.py +++ b/pokemongo_bot/websocket_remote_control.py @@ -42,7 +42,7 @@ def on_remote_command(self, command): command_handler() def get_player_info(self): - request = self.api.create_request() + request = self.bot.api.create_request() request.get_player() request.get_inventory() response_dict = request.call() From 9c1a9435daaf547acd069a5c40270191b2a16d60 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Wed, 10 Aug 2016 02:13:21 +0200 Subject: [PATCH 126/202] let's use dict.get a bit to avoid errors --- brantje.py | 9 +++++++++ pokemongo_bot/websocket_remote_control.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 brantje.py diff --git a/brantje.py b/brantje.py new file mode 100644 index 0000000000..0674ceb077 --- /dev/null +++ b/brantje.py @@ -0,0 +1,9 @@ +# coding: utf-8 +from socketIO_client import SocketIO +s = SocketIO('localhost', 4000) +def echo(msg): + print msg + +s.on('get_player_info:d.camata@gmail.com', echo) +s.emit('remote:send_request', {'account': 'd.camata@gmail.com', 'name': 'get_player_info'}) +s.wait(1) diff --git a/pokemongo_bot/websocket_remote_control.py b/pokemongo_bot/websocket_remote_control.py index 023db22ff4..cd5bd5af96 100644 --- a/pokemongo_bot/websocket_remote_control.py +++ b/pokemongo_bot/websocket_remote_control.py @@ -46,8 +46,8 @@ def get_player_info(self): request.get_player() request.get_inventory() response_dict = request.call() - inventory = response_dict['responses']['GET_INVENTORY'] - player_info = response_dict['responses']['GET_PLAYER'] + inventory = response_dict['responses'].get('GET_INVENTORY', {}) + player_info = response_dict['responses'].get('GET_PLAYER', {}) self.sio.emit( 'bot:send_reply', { From 61df52fb4d3b517620fcb48be20345ecadc7e295 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Wed, 10 Aug 2016 02:13:55 +0200 Subject: [PATCH 127/202] keeping the account in the remote command response --- pokemongo_bot/socketio_server/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/socketio_server/app.py b/pokemongo_bot/socketio_server/app.py index 7f3c5040d2..a970a30479 100644 --- a/pokemongo_bot/socketio_server/app.py +++ b/pokemongo_bot/socketio_server/app.py @@ -20,7 +20,7 @@ def remote_control(sid, command): @sio.on('bot:send_reply') def request_reply(sid, response): event = response.pop('command') - account = response.pop('account') + account = response['account'] event = "{}:{}".format(event, account) sio.emit(event, response) From 9f9146c0f9e418c3ab07ab177983c556651b7ed6 Mon Sep 17 00:00:00 2001 From: Eevee Date: Wed, 10 Aug 2016 09:15:04 +0900 Subject: [PATCH 128/202] Add ColoredLoggingHandler (#3198) --- CONTRIBUTORS.md | 1 + configs/config.json.cluster.example | 1 + configs/config.json.example | 1 + configs/config.json.map.example | 1 + configs/config.json.path.example | 1 + configs/config.json.pokemon.example | 1 + pokecli.py | 8 + pokemongo_bot/__init__.py | 10 +- pokemongo_bot/event_handlers/__init__.py | 1 + .../event_handlers/colored_logging_handler.py | 172 ++++++++++++++++++ 10 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 pokemongo_bot/event_handlers/colored_logging_handler.py diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 57b289ab90..bb0ac15b7b 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -59,3 +59,4 @@ * nikhil-pandey * thebigjc * JaapMoolenaar + * eevee-github diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index e4597519f6..eb507cd43c 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -81,6 +81,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.example b/configs/config.json.example index d2e8d4f064..08c844218c 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -89,6 +89,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.map.example b/configs/config.json.map.example index 56a006450c..cf6604976b 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -323,6 +323,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 6f7b04c305..6b6619573b 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -83,6 +83,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "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 e7ba38dc37..1dfa01199d 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -89,6 +89,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or" }, diff --git a/pokecli.py b/pokecli.py index 6811da7696..f59ae3acb9 100644 --- a/pokecli.py +++ b/pokecli.py @@ -395,6 +395,14 @@ def _json_loader(filename): type=float, default=5.0 ) + add_config( + parser, + load, + long_flag="--logging_color", + help="If logging_color is set to true, colorized logging handler will be used", + type=bool, + default=True + ) # Start to parse other attrs config = parser.parse_args() diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 133bb4631e..31e7eb0bd1 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -25,7 +25,7 @@ from human_behaviour import sleep from item_list import Item from metrics import Metrics -from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler +from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler, ColoredLoggingHandler from pokemongo_bot.socketio_server.runner import SocketIoRunner from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl from pokemongo_bot.base_dir import _base_dir @@ -88,7 +88,12 @@ def start(self): random.seed() def _setup_event_system(self): - handlers = [LoggingHandler()] + handlers = [] + if self.config.logging_color: + handlers.append(ColoredLoggingHandler()) + else: + handlers.append(LoggingHandler()) + if self.config.websocket_server_url: if self.config.websocket_start_embedded_server: self.sio_runner = SocketIoRunner(self.config.websocket_server_url) @@ -103,7 +108,6 @@ def _setup_event_system(self): if self.config.websocket_remote_control: remote_control = WebsocketRemoteControl(self).start() - self.event_manager = EventManager(*handlers) self._register_events() if self.config.show_events: diff --git a/pokemongo_bot/event_handlers/__init__.py b/pokemongo_bot/event_handlers/__init__.py index f0933a0e68..a4dd6fce7f 100644 --- a/pokemongo_bot/event_handlers/__init__.py +++ b/pokemongo_bot/event_handlers/__init__.py @@ -1,2 +1,3 @@ from logging_handler import LoggingHandler from socketio_handler import SocketIoHandler +from colored_logging_handler import ColoredLoggingHandler diff --git a/pokemongo_bot/event_handlers/colored_logging_handler.py b/pokemongo_bot/event_handlers/colored_logging_handler.py new file mode 100644 index 0000000000..80e5414ae6 --- /dev/null +++ b/pokemongo_bot/event_handlers/colored_logging_handler.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import time +import sys +import struct + +from pokemongo_bot.event_manager import EventHandler + +class ColoredLoggingHandler(EventHandler): + EVENT_COLOR_MAP = { + 'api_error': 'red', + 'bot_exit': 'red', + 'bot_start': 'green', + 'config_error': 'red', + 'egg_already_incubating': 'yellow', + 'egg_hatched': 'green', + 'future_pokemon_release': 'yellow', + 'incubate': 'green', + 'incubator_already_used': 'yellow', + 'inventory_full': 'yellow', + 'item_discard_fail': 'red', + 'item_discarded': 'green', + 'keep_best_release': 'green', + 'level_up': 'green', + 'level_up_reward': 'green', + 'location_cache_error': 'yellow', + 'location_cache_ignored': 'yellow', + 'login_failed': 'red', + 'login_successful': 'green', + 'lucky_egg_error': 'red', + 'move_to_map_pokemon_encounter': 'green', + 'move_to_map_pokemon_fail': 'red', + 'next_egg_incubates': 'yellow', + 'next_sleep': 'green', + 'no_pokeballs': 'red', + 'pokemon_appeared': 'yellow', + 'pokemon_capture_failed': 'red', + 'pokemon_caught': 'blue', + 'pokemon_evolved': 'green', + 'pokemon_fled': 'red', + 'pokemon_inventory_full': 'red', + 'pokemon_nickname_invalid': 'red', + 'pokemon_not_in_range': 'yellow', + 'pokemon_release': 'green', + 'pokemon_vanished': 'red', + 'pokestop_empty': 'yellow', + 'pokestop_searching_too_often': 'yellow', + 'rename_pokemon': 'green', + 'skip_evolve': 'yellow', + 'softban': 'red', + 'spun_pokestop': 'cyan', + 'threw_berry_failed': 'red', + 'unknown_spin_result': 'red', + 'unset_pokemon_nickname': 'red', + 'vip_pokemon': 'red', + + # event names for 'white' still here to remember that these events are already determined its color. + 'arrived_at_cluster': 'white', + 'arrived_at_fort': 'white', + 'bot_sleep': 'white', + 'catchable_pokemon': 'white', + 'found_cluster': 'white', + 'incubate_try': 'white', + 'load_cached_location': 'white', + 'location_found': 'white', + 'login_started': 'white', + 'lured_pokemon_found': 'white', + 'move_to_map_pokemon_move_towards': 'white', + 'move_to_map_pokemon_teleport_back': 'white', + 'move_to_map_pokemon_updated_map': 'white', + 'moving_to_fort': 'white', + 'moving_to_lured_fort': 'white', + 'pokemon_catch_rate': 'white', + 'pokemon_evolve_fail': 'white', + 'pokestop_on_cooldown': 'white', + 'pokestop_out_of_range': 'white', + 'polyline_request': 'white', + 'position_update': 'white', + 'set_start_location': 'white', + 'softban_fix': 'white', + 'softban_fix_done': 'white', + 'spun_fort': 'white', + 'threw_berry': 'white', + 'threw_pokeball': 'white', + 'used_lucky_egg': 'white' + } + CONTINUOUS_EVENT_NAMES = [ + 'catchable_pokemon', + 'moving_to_lured_fort', + 'spun_fort' + ] + COLOR_CODE = { + 'red': '91', + 'green': '92', + 'yellow': '93', + 'blue': '94', + 'cyan': '96' + } + + def __init__(self): + self._last_event = None + try: + # this `try ... except` is for ImportError on Windows + import fcntl + import termios + self._ioctl = fcntl.ioctl + self._TIOCGWINSZ = termios.TIOCGWINSZ + except ImportError: + self._ioctl = None + self._TIOCGWINSZ = None + + def handle_event(self, event, sender, level, formatted_msg, data): + # Prepare message string + message = None + if formatted_msg: + try: + message = formatted_msg.decode('utf-8') + except UnicodeEncodeError: + message = formatted_msg + else: + message = '{}'.format(str(data)) + + # Replace message if necessary + if event == 'catchable_pokemon': + message = 'Something rustles nearby!' + + # Truncate previous line if same event continues + if event in ColoredLoggingHandler.CONTINUOUS_EVENT_NAMES and self._last_event == event: + # Filling with "' ' * terminal_width" in order to completely clear last line + terminal_width = self._terminal_width() + if terminal_width: + sys.stdout.write('\r{}\r'.format(' ' * terminal_width)) + else: + sys.stdout.write('\r') + else: + sys.stdout.write("\n") + + color_name = None + if event in ColoredLoggingHandler.EVENT_COLOR_MAP: + color_name = ColoredLoggingHandler.EVENT_COLOR_MAP[event] + + # Change color if necessary + if event == 'egg_hatched' and data.get('pokemon', 'error') == 'error': + # `egg_hatched` event will be dispatched in both cases: hatched pokemon info is successfully taken or not. + # change color from 'green' to 'red' in case of error. + color_name = 'red' + + if color_name in ColoredLoggingHandler.COLOR_CODE: + sys.stdout.write( + '[{time}] \033[{color}m{message}\033[0m'.format( + time=time.strftime("%H:%M:%S"), + color=ColoredLoggingHandler.COLOR_CODE[color_name], + message=message + ) + ) + else: + sys.stdout.write('[{time}] {message}'.format( + time=time.strftime("%H:%M:%S"), + message=message + )) + + sys.stdout.flush() + self._last_event = event + + def _terminal_width(self): + if self._ioctl is None or self._TIOCGWINSZ is None: + return None + + h, w, hp, wp = struct.unpack('HHHH', + self._ioctl(0, self._TIOCGWINSZ, + struct.pack('HHHH', 0, 0, 0, 0))) + return w From 8b5af8d766ac161be926932b05e3fc785a33661d Mon Sep 17 00:00:00 2001 From: achretien Date: Wed, 10 Aug 2016 02:17:46 +0200 Subject: [PATCH 129/202] Update TransferPokemon to use new Inventory Class (#3320) * Update TransferPokemon to use new Inventory Class * Use base_dir * Don't release pokemon if bot is on test mode --- .../cell_workers/transfer_pokemon.py | 202 +++++++----------- pokemongo_bot/inventory.py | 15 +- 2 files changed, 91 insertions(+), 126 deletions(-) diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 88548e75b2..6b3eebac39 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -1,9 +1,10 @@ import json import os -from pokemongo_bot.base_dir import _base_dir +from pokemongo_bot import inventory from pokemongo_bot.human_behaviour import action_delay from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.inventory import Pokemons class TransferPokemon(BaseTask): @@ -11,129 +12,75 @@ class TransferPokemon(BaseTask): def work(self): pokemon_groups = self._release_pokemon_get_groups() - for pokemon_id in pokemon_groups: - group = pokemon_groups[pokemon_id] - - if len(group) > 0: - pokemon_name = self.bot.pokemon_list[pokemon_id - 1]['Name'] - keep_best, keep_best_cp, keep_best_iv = self._validate_keep_best_config(pokemon_name) - - if keep_best: - best_pokemon_ids = set() - order_criteria = 'none' - if keep_best_cp >= 1: - cp_limit = keep_best_cp - best_cp_pokemons = sorted(group, key=lambda x: (x['cp'], x['iv']), reverse=True)[:cp_limit] - best_pokemon_ids = set(pokemon['pokemon_data']['id'] for pokemon in best_cp_pokemons) - order_criteria = 'cp' - - if keep_best_iv >= 1: - iv_limit = keep_best_iv - best_iv_pokemons = sorted(group, key=lambda x: (x['iv'], x['cp']), reverse=True)[:iv_limit] - best_pokemon_ids |= set(pokemon['pokemon_data']['id'] for pokemon in best_iv_pokemons) - if order_criteria == 'cp': - order_criteria = 'cp and iv' - else: - order_criteria = 'iv' - - # remove best pokemons from all pokemons array - all_pokemons = group - best_pokemons = [] - for best_pokemon_id in best_pokemon_ids: - for pokemon in all_pokemons: - if best_pokemon_id == pokemon['pokemon_data']['id']: - all_pokemons.remove(pokemon) - best_pokemons.append(pokemon) - - transfer_pokemons = [pokemon for pokemon in all_pokemons - if self.should_release_pokemon(pokemon_name, - pokemon['cp'], - pokemon['iv'], - True)] - - if transfer_pokemons: - if best_pokemons: - self.emit_event( - 'keep_best_release', - formatted="Keeping best {amount} {pokemon}, based on {criteria}", - data={ - 'amount': len(best_pokemons), - 'pokemon': pokemon_name, - 'criteria': order_criteria - } - ) - for pokemon in transfer_pokemons: - self.release_pokemon(pokemon_name, pokemon['cp'], pokemon['iv'], pokemon['pokemon_data']['id']) - else: - group = sorted(group, key=lambda x: x['cp'], reverse=True) - for item in group: - pokemon_cp = item['cp'] - pokemon_potential = item['iv'] - - if self.should_release_pokemon(pokemon_name, pokemon_cp, pokemon_potential): - self.release_pokemon(pokemon_name, item['cp'], item['iv'], item['pokemon_data']['id']) + for pokemon_id, group in pokemon_groups.iteritems(): + pokemon_name = Pokemons.name_for(pokemon_id) + keep_best, keep_best_cp, keep_best_iv = self._validate_keep_best_config(pokemon_name) + + if keep_best: + best_pokemon_ids = set() + order_criteria = 'none' + if keep_best_cp >= 1: + cp_limit = keep_best_cp + best_cp_pokemons = sorted(group, key=lambda x: (x.cp, x.iv), reverse=True)[:cp_limit] + best_pokemon_ids = set(pokemon.id for pokemon in best_cp_pokemons) + order_criteria = 'cp' + + if keep_best_iv >= 1: + iv_limit = keep_best_iv + best_iv_pokemons = sorted(group, key=lambda x: (x.iv, x.cp), reverse=True)[:iv_limit] + best_pokemon_ids |= set(pokemon.id for pokemon in best_iv_pokemons) + if order_criteria == 'cp': + order_criteria = 'cp and iv' + else: + order_criteria = 'iv' + + # remove best pokemons from all pokemons array + all_pokemons = group + best_pokemons = [] + for best_pokemon_id in best_pokemon_ids: + for pokemon in all_pokemons: + if best_pokemon_id == pokemon.id: + all_pokemons.remove(pokemon) + best_pokemons.append(pokemon) + + transfer_pokemons = [pokemon for pokemon in all_pokemons if self.should_release_pokemon(pokemon,True)] + + if transfer_pokemons: + if best_pokemons: + self.emit_event( + 'keep_best_release', + formatted="Keeping best {amount} {pokemon}, based on {criteria}", + data={ + 'amount': len(best_pokemons), + 'pokemon': pokemon_name, + 'criteria': order_criteria + } + ) + for pokemon in transfer_pokemons: + self.release_pokemon(pokemon) + else: + group = sorted(group, key=lambda x: x.cp, reverse=True) + for pokemon in group: + if self.should_release_pokemon(pokemon): + self.release_pokemon(pokemon) def _release_pokemon_get_groups(self): pokemon_groups = {} - 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 - - inventory_dict = inventory_req['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] - - 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_dict, outfile) - - for pokemon in inventory_dict: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data", "pokemon_id" - ], pokemon) - except KeyError: - continue - - pokemon_data = pokemon['inventory_item_data']['pokemon_data'] - - # pokemon in fort, so we cant transfer it - if 'deployed_fort_id' in pokemon_data and pokemon_data['deployed_fort_id']: - continue - - # favorite pokemon can't transfer in official game client - if pokemon_data.get('favorite', 0) is 1: + for pokemon in inventory.pokemons(True).all(): + if pokemon.in_fort or pokemon.is_favorite: continue - group_id = pokemon_data['pokemon_id'] - group_pokemon_cp = pokemon_data['cp'] - group_pokemon_iv = self.get_pokemon_potential(pokemon_data) + group_id = pokemon.pokemon_id if group_id not in pokemon_groups: pokemon_groups[group_id] = [] - pokemon_groups[group_id].append({ - 'cp': group_pokemon_cp, - 'iv': group_pokemon_iv, - 'pokemon_data': pokemon_data - }) + pokemon_groups[group_id].append(pokemon) return pokemon_groups - def get_pokemon_potential(self, pokemon_data): - total_iv = 0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - for individual_stat in iv_stats: - try: - total_iv += pokemon_data[individual_stat] - except Exception: - continue - return round((total_iv / 45.0), 2) - - def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): - release_config = self._get_release_config_for(pokemon_name) + def should_release_pokemon(self, pokemon, keep_best_mode = False): + release_config = self._get_release_config_for(pokemon.name) if (keep_best_mode and not release_config.has_key('never_release') @@ -158,11 +105,11 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): return True release_cp = release_config.get('release_below_cp', 0) - if cp < release_cp: + if pokemon.cp < release_cp: release_results['cp'] = True release_iv = release_config.get('release_below_iv', 0) - if iv < release_iv: + if pokemon.iv < release_iv: release_results['iv'] = True logic_to_function = { @@ -175,9 +122,9 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): 'future_pokemon_release', formatted="Releasing {pokemon} (CP {cp}/IV {iv}) based on rule: CP < {below_cp} {cp_iv_logic} IV < {below_iv}", data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': iv, + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv, 'below_cp': release_cp, 'cp_iv_logic': cp_iv_logic.upper(), 'below_iv': release_iv @@ -186,16 +133,27 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): return logic_to_function[cp_iv_logic](*release_results.values()) - def release_pokemon(self, pokemon_name, cp, iv, pokemon_id): - response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon_id) + def release_pokemon(self, pokemon): + try: + if self.bot.config.test: + candy_awarded = 1 + else: + response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon.id) + candy_awarded = response_dict['responses']['RELEASE_POKEMON']['candy_awarded'] + except KeyError: + return + + # We could refresh here too, but adding 1 saves a inventory request + candy = inventory.candies().get(pokemon.pokemon_id) + candy.add(candy_awarded) self.bot.metrics.released_pokemon() self.emit_event( 'pokemon_release', formatted='Exchanged {pokemon} [CP {cp}] [IV {iv}] for candy.', data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': iv + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv } ) action_delay(self.bot.config.action_wait_min, self.bot.config.action_wait_max) diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index 1fc208d235..ea81b7c093 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -164,9 +164,11 @@ def __init__(self, data): self._static_data = Pokemons.data_for(self.pokemon_id) self.name = Pokemons.name_for(self.pokemon_id) self.iv = self._compute_iv() + self.in_fort = 'deployed_fort_id' in data + self.is_favorite = data.get('favorite', 0) is 1 def can_evolve_now(self): - return self.has_next_evolution() and self.candy_quantity > self.evolution_cost + 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 @@ -217,11 +219,14 @@ 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'] + 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) + 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) + _inventory = None @@ -244,7 +249,9 @@ def candies(refresh=False): return _inventory.candy -def pokemons(): +def pokemons(refresh=False): + if refresh: + refresh_inventory() return _inventory.pokemons From 310f5786049318050f424358c144829f5d995e37 Mon Sep 17 00:00:00 2001 From: Simon Shi Date: Wed, 10 Aug 2016 12:53:30 +0800 Subject: [PATCH 130/202] Some text fixes for setup.sh (#3390) Minor text fix. --- setup.sh | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/setup.sh b/setup.sh index bcff0feefb..07589917e9 100755 --- a/setup.sh +++ b/setup.sh @@ -37,7 +37,7 @@ Input location " location read -p "Input gmapkey " gmapkey -cp configs/config.json.example configs/config.json +cp -f configs/config.json.example configs/config.json if [ "$auth" = "2" ] then sed -i "s/google/ptc/g" configs/config.json @@ -46,7 +46,7 @@ sed -i "s/YOUR_USERNAME/$username/g" configs/config.json sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json sed -i "s/SOME_LOCATION/$location/g" configs/config.json sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json -echo "Edit configs/config.json to modify any other config." +echo "Edit ./configs/config.json to modify any other config." } function Pokebotinstall () { @@ -67,12 +67,14 @@ echo "You are on Mac os" sudo brew update sudo brew install --devel protobuf else -echo "Nothing happend." +echo "Please check if you have python pip protobuf gcc make installed on your device." +echo "Wait 5 seconds to continue or Use ctrl+c to interrupt this shell." +sleep 5 fi sudo pip install virtualenv Pokebotupdate Pokebotencrypt -echo "Install complete." +echo "Install complete. Starting to generate config.json." Pokebotconfig } @@ -89,7 +91,7 @@ echo " -i,--install. Install PokemonGo-Bot." echo " -b,--backup. Backup config files." echo " -c,--config. Easy config generator." echo " -e,--encrypt. Make encrypt.so." -echo " -r,--reset. Force sync dev branch." +echo " -r,--reset. Force sync dev branch." echo " -u,--update. Command git pull to update." } @@ -120,12 +122,13 @@ Pokebothelp ;; *.json) filename=$* +echo "It's better to use run.sh, not this one." cd $pokebotpath if [ ! -f ./configs/"$filename" ] then -echo "There's no ./configs/"$filename" file. It's better to use run.sh not this one." +echo "There's no ./configs/"$filename" file. It's better to use run.sh, not this one." else -Pokebotrun +./run ./configs/"$filename" fi ;; *) From b3f9e0f2951e55342a8f4a2036c333c2189571c3 Mon Sep 17 00:00:00 2001 From: Simon Shi Date: Wed, 10 Aug 2016 13:40:39 +0800 Subject: [PATCH 131/202] Fix path of shells in install.sh (#3393) * Update install.sh Fix path * Update setup.sh fix path * Update run.sh fix path --- install.sh | 10 ++++++---- run.sh | 2 +- setup.sh | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/install.sh b/install.sh index 32cc1e1124..1e7415ed30 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -pokebotpath=$(pwd) +pokebotpath=$(cd "$(dirname "$0")"; pwd) cd $pokebotpath if [ -f /etc/debian_version ] then @@ -17,7 +17,9 @@ echo "You are on Mac os" sudo brew update sudo brew install --devel protobuf else -echo "Nothing happend." +echo "Please check if you have python pip protobuf gcc make installed on your device." +echo "Wait 5 seconds to continue or Use ctrl+c to interrupt this shell." +sleep 5 fi pip install virtualenv cd $pokebotpath @@ -36,7 +38,7 @@ mv libencrypt.so $pokebotpath/encrypt.so cd ../.. rm -rf pgoencrypt.tar.gz rm -rf pgoencrypt -echo "Install complete." +echo "Install complete. Starting to generate config.json." cd $pokebotpath read -p "1.google 2.ptc " auth @@ -58,5 +60,5 @@ sed -i "s/YOUR_USERNAME/$username/g" configs/config.json sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json sed -i "s/SOME_LOCATION/$location/g" configs/config.json sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json -echo "Edit configs/config.json to modify any other config. Use run.sh to run." +echo "Edit configs/config.json to modify any other config. Use run.sh ./configs/config.json to run." exit 0 diff --git a/run.sh b/run.sh index d8406697e7..4c93cda234 100755 --- a/run.sh +++ b/run.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -pokebotpath=$(pwd) +pokebotpath=$(cd "$(dirname "$0")"; pwd) filename="" if [ ! -z $1 ]; then filename=$1 diff --git a/setup.sh b/setup.sh index 07589917e9..84baef623a 100755 --- a/setup.sh +++ b/setup.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -pokebotpath=$(pwd) -backuppath=$(pwd)"/backup" +pokebotpath=$(cd "$(dirname "$0")"; pwd) +backuppath=$pokebotpath"/backup" function Pokebotupdate () { cd $pokebotpath From 67fe00bcfc04fb6ddd2c78ca208aad4e4cbf2523 Mon Sep 17 00:00:00 2001 From: jebabin Date: Wed, 10 Aug 2016 07:42:14 +0200 Subject: [PATCH 132/202] Fix evolution error in pokemon.json (#3344) Fix evolution error in pokemon.json --- data/pokemon.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/data/pokemon.json b/data/pokemon.json index 58f7c437f2..e64b7c31fb 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -1600,8 +1600,8 @@ }, "Next evolution(s)": [ { - "Number": "039", - "Name": "Jigglypuff" + "Number": "040", + "Name": "Wigglytuff" } ], "Special Attack(s)": [ @@ -1633,8 +1633,8 @@ "Height": "1.0 m", "Previous evolution(s)": [ { - "Number": "040", - "Name": "Wigglytuff" + "Number": "039", + "Name": "Jigglypuff" } ], "Special Attack(s)": [ @@ -4820,8 +4820,8 @@ }, "Next evolution(s)": [ { - "Number": "120", - "Name": "Staryu" + "Number": "121", + "Name": "Starmie" } ], "Special Attack(s)": [ @@ -4860,8 +4860,8 @@ "Height": "1.1 m", "Previous evolution(s)": [ { - "Number": "121", - "Name": "Starmie" + "Number": "120", + "Name": "Staryu" } ], "Special Attack(s)": [ From 01bc14de0543196de6688dc6bd560739db59fd14 Mon Sep 17 00:00:00 2001 From: Sean Quinn Date: Wed, 10 Aug 2016 03:18:09 -0400 Subject: [PATCH 133/202] Improve formatting consistency in transfer_pokemon.py (#3397) Improve formatting consistency --- pokemongo_bot/cell_workers/transfer_pokemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 6b3eebac39..9e970d7d7f 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -120,7 +120,7 @@ def should_release_pokemon(self, pokemon, keep_best_mode = False): if logic_to_function[cp_iv_logic](*release_results.values()): self.emit_event( 'future_pokemon_release', - formatted="Releasing {pokemon} (CP {cp}/IV {iv}) based on rule: CP < {below_cp} {cp_iv_logic} IV < {below_iv}", + formatted="Releasing {pokemon} [CP {cp}] [IV {iv}] based on rule: CP < {below_cp} {cp_iv_logic} IV < {below_iv}", data={ 'pokemon': pokemon.name, 'cp': pokemon.cp, From d22c5b2c4b9b0602819d50dc03496aafd1f628e6 Mon Sep 17 00:00:00 2001 From: brantje Date: Wed, 10 Aug 2016 11:47:27 +0200 Subject: [PATCH 134/202] Remove unnecessary file --- brantje.py | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 brantje.py diff --git a/brantje.py b/brantje.py deleted file mode 100644 index 0674ceb077..0000000000 --- a/brantje.py +++ /dev/null @@ -1,9 +0,0 @@ -# coding: utf-8 -from socketIO_client import SocketIO -s = SocketIO('localhost', 4000) -def echo(msg): - print msg - -s.on('get_player_info:d.camata@gmail.com', echo) -s.emit('remote:send_request', {'account': 'd.camata@gmail.com', 'name': 'get_player_info'}) -s.wait(1) From 0c3c4c004d5081b91ffe278850d1b1821e4f63b4 Mon Sep 17 00:00:00 2001 From: Simon Shi Date: Wed, 10 Aug 2016 17:58:03 +0800 Subject: [PATCH 135/202] Put info on the next line in run.sh (#3422) * Update setup.sh fix typo * Update run.sh fix typo --- run.sh | 5 +++-- setup.sh | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/run.sh b/run.sh index 4c93cda234..becbf01f05 100755 --- a/run.sh +++ b/run.sh @@ -15,7 +15,8 @@ while true do cd $pokebotpath python pokecli.py -cf $filename -read -p "Press any button or wait 20 seconds." -r -s -n1 -t 20 -echo `date`"Pokebot"$*" Stopped." +read -p "Press any button or wait 20 seconds to continue. +" -r -s -n1 -t 20 +echo `date`" Pokebot"$*" Stopped." done exit 0 diff --git a/setup.sh b/setup.sh index 84baef623a..fef667bd2d 100755 --- a/setup.sh +++ b/setup.sh @@ -128,7 +128,7 @@ if [ ! -f ./configs/"$filename" ] then echo "There's no ./configs/"$filename" file. It's better to use run.sh, not this one." else -./run ./configs/"$filename" +./run.sh ./configs/"$filename" fi ;; *) From 72622b438ebd65283ee14d136e2ee610612c43f6 Mon Sep 17 00:00:00 2001 From: vanishing Date: Wed, 10 Aug 2016 08:37:33 -0400 Subject: [PATCH 136/202] Fix Struct() argument 1 must be string, not unicode. (#3375) --- CONTRIBUTORS.md | 1 + pokemongo_bot/event_handlers/colored_logging_handler.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index bb0ac15b7b..a963396fcb 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -60,3 +60,4 @@ * thebigjc * JaapMoolenaar * eevee-github + * g0vanish diff --git a/pokemongo_bot/event_handlers/colored_logging_handler.py b/pokemongo_bot/event_handlers/colored_logging_handler.py index 80e5414ae6..dd7239d6d6 100644 --- a/pokemongo_bot/event_handlers/colored_logging_handler.py +++ b/pokemongo_bot/event_handlers/colored_logging_handler.py @@ -166,7 +166,7 @@ def _terminal_width(self): if self._ioctl is None or self._TIOCGWINSZ is None: return None - h, w, hp, wp = struct.unpack('HHHH', + h, w, hp, wp = struct.unpack(str('HHHH'), self._ioctl(0, self._TIOCGWINSZ, - struct.pack('HHHH', 0, 0, 0, 0))) + struct.pack(str('HHHH'), 0, 0, 0, 0))) return w From 558540ed1a9723345f1a25b4f363b441424cbc30 Mon Sep 17 00:00:00 2001 From: achretien Date: Wed, 10 Aug 2016 19:03:04 +0200 Subject: [PATCH 137/202] Give the possibility to disable a task without removing it (#3417) * Give the possiblity to disable a task in config without removing it from the config file * Put exmple only in nickname task * Add Unit testing * typo * Use enabled false as exemple --- configs/config.json.example | 7 +++++++ pokemongo_bot/base_task.py | 1 + pokemongo_bot/tree_config_builder.py | 3 ++- tests/tree_config_builder_test.py | 19 +++++++++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/configs/config.json.example b/configs/config.json.example index 08c844218c..59974ba156 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -21,6 +21,13 @@ { "type": "TransferPokemon" }, + { + "type": "NicknamePokemon", + "config": { + "enabled": false, + "nickname_template": "{iv_pct}_{iv_ads}" + } + }, { "type": "EvolvePokemon", "config": { diff --git a/pokemongo_bot/base_task.py b/pokemongo_bot/base_task.py index 22bbedf4e8..1b610d31aa 100644 --- a/pokemongo_bot/base_task.py +++ b/pokemongo_bot/base_task.py @@ -9,6 +9,7 @@ def __init__(self, bot, config): self.config = config self._validate_work_exists() self.logger = logging.getLogger(type(self).__name__) + self.enabled = config.get('enabled', True) self.initialize() def _validate_work_exists(self): diff --git a/pokemongo_bot/tree_config_builder.py b/pokemongo_bot/tree_config_builder.py index 6242747b25..57dc9da33c 100644 --- a/pokemongo_bot/tree_config_builder.py +++ b/pokemongo_bot/tree_config_builder.py @@ -61,7 +61,8 @@ def build(self): ) instance = worker(self.bot, task_config) - workers.append(instance) + if instance.enabled: + workers.append(instance) return workers diff --git a/tests/tree_config_builder_test.py b/tests/tree_config_builder_test.py index 1992c8187c..f8982cbfad 100644 --- a/tests/tree_config_builder_test.py +++ b/tests/tree_config_builder_test.py @@ -86,6 +86,25 @@ def test_task_with_config(self): tree = builder.build() self.assertTrue(tree[0].config.get('longer_eggs_first', False)) + def test_disabling_task(self): + obj = convert_from_json("""[{ + "type": "HandleSoftBan", + "config": { + "enabled": false + } + }, { + "type": "CatchLuredPokemon", + "config": { + "enabled": true + } + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + tree = builder.build() + + self.assertTrue(len(tree) == 1) + self.assertIsInstance(tree[0], CatchLuredPokemon) + def test_load_plugin_task(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') plugin_loader = PluginLoader() From 99187abd7d4407ae57a69e4f0ad9126d60f70236 Mon Sep 17 00:00:00 2001 From: nivong Date: Wed, 10 Aug 2016 19:25:13 +0200 Subject: [PATCH 138/202] fix config creation (#3482) Changed auth to be more specifik and added right permissions. --- setup.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.sh b/setup.sh index fef667bd2d..5fce474b21 100755 --- a/setup.sh +++ b/setup.sh @@ -26,7 +26,7 @@ rm -rf pgoencrypt function Pokebotconfig () { cd $pokebotpath -read -p "1.google 2.ptc +read -p "enter 1 for google or 2 for ptc " auth read -p "Input username " username @@ -37,7 +37,7 @@ Input location " location read -p "Input gmapkey " gmapkey -cp -f configs/config.json.example configs/config.json +cp -f configs/config.json.example configs/config.json && chmod 755 if [ "$auth" = "2" ] then sed -i "s/google/ptc/g" configs/config.json From edeb2c23f3bcae117dab99b1f547a351bc43af8b Mon Sep 17 00:00:00 2001 From: leadboots5 Date: Wed, 10 Aug 2016 12:56:52 -0500 Subject: [PATCH 139/202] Remove unused IV calculation from evolve_pokemon (#3487) Previously IV was computed in each worker. Now its fetched from inventory. This was left over and not called in the worker at all. --- pokemongo_bot/cell_workers/evolve_pokemon.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 4cc451115b..7380f1c5db 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -117,8 +117,3 @@ def _execute_pokemon_evolve(self, pokemon, cache): cache[pokemon.name] = 1 sleep(0.7) return False - - def _compute_iv(self, pokemon): - total_iv = pokemon.get("individual_attack", 0) + pokemon.get("individual_stamina", 0) + pokemon.get( - "individual_defense", 0) - return round((total_iv / 45.0), 2) From f823d77b091b8f4bb047afabfec745a24af51d13 Mon Sep 17 00:00:00 2001 From: achretien Date: Wed, 10 Aug 2016 20:05:10 +0200 Subject: [PATCH 140/202] Don't show Inventory full event if we set "ignore_item_count" (#3440) --- pokemongo_bot/cell_workers/move_to_fort.py | 2 +- pokemongo_bot/cell_workers/spin_fort.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 7dcd0977b1..2be287e86c 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -19,7 +19,7 @@ def initialize(self): def should_run(self): has_space_for_loot = self.bot.has_space_for_loot() - if not has_space_for_loot: + if not has_space_for_loot and not self.ignore_item_count: self.emit_event( 'inventory_full', formatted="Inventory is full. You might want to change your config to recycle more items if this message appears consistently." diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 445946e7e1..9422d8ec35 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -19,7 +19,7 @@ def initialize(self): self.ignore_item_count = self.config.get("ignore_item_count", False) def should_run(self): - if not self.bot.has_space_for_loot(): + if not self.bot.has_space_for_loot() and not self.ignore_item_count: self.emit_event( 'inventory_full', formatted="Inventory is full. You might want to change your config to recycle more items if this message appears consistently." @@ -106,10 +106,11 @@ def work(self): data={'pokestop': fort_name, 'minutes_left': minutes_left} ) elif spin_result == 4: - self.emit_event( - 'inventory_full', - formatted="Inventory is full!" - ) + if not self.ignore_item_count: + self.emit_event( + 'inventory_full', + formatted="Inventory is full!" + ) else: self.emit_event( 'unknown_spin_result', From 9ae1b785f0836d8769d011626d2bc071ed338bff Mon Sep 17 00:00:00 2001 From: Simon Shi Date: Thu, 11 Aug 2016 02:11:34 +0800 Subject: [PATCH 141/202] Fix showing the date in run.sh (#3433) fix the logic of showing the date --- run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.sh b/run.sh index becbf01f05..9938f8c5e0 100755 --- a/run.sh +++ b/run.sh @@ -15,8 +15,8 @@ while true do cd $pokebotpath python pokecli.py -cf $filename +echo `date`" Pokebot "$*" Stopped." read -p "Press any button or wait 20 seconds to continue. " -r -s -n1 -t 20 -echo `date`" Pokebot"$*" Stopped." done exit 0 From 1183b934708cb98cb7e05dc12d2c30a034dc0229 Mon Sep 17 00:00:00 2001 From: Dmitry Ovodov Date: Thu, 11 Aug 2016 05:35:16 +0700 Subject: [PATCH 142/202] Typo fix: show new catch rate after berry throw. (#3521) --- pokemongo_bot/cell_workers/pokemon_catch_worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 345fcabc7e..3b2092e535 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -251,7 +251,7 @@ def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, cu data={ 'berry_name': self.item_list[str(berry_id)], 'ball_name': self.item_list[str(current_ball)], - 'new_catch_rate': self._pct(catch_rate_by_ball[current_ball]) + 'new_catch_rate': self._pct(new_catch_rate_by_ball[current_ball]) } ) From 41e5758961d7308f68aed4bad137f1201e5ed209 Mon Sep 17 00:00:00 2001 From: net8q Date: Thu, 11 Aug 2016 00:36:46 +0200 Subject: [PATCH 143/202] Fix stdout is not a terminal (#3511) --- pokemongo_bot/event_handlers/colored_logging_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/event_handlers/colored_logging_handler.py b/pokemongo_bot/event_handlers/colored_logging_handler.py index dd7239d6d6..f3c902d464 100644 --- a/pokemongo_bot/event_handlers/colored_logging_handler.py +++ b/pokemongo_bot/event_handlers/colored_logging_handler.py @@ -125,7 +125,7 @@ def handle_event(self, event, sender, level, formatted_msg, data): message = 'Something rustles nearby!' # Truncate previous line if same event continues - if event in ColoredLoggingHandler.CONTINUOUS_EVENT_NAMES and self._last_event == event: + if event in ColoredLoggingHandler.CONTINUOUS_EVENT_NAMES and self._last_event == event and sys.stdout.isatty(): # Filling with "' ' * terminal_width" in order to completely clear last line terminal_width = self._terminal_width() if terminal_width: From e60da103cc40c68be8b401f2bc4c63e696210047 Mon Sep 17 00:00:00 2001 From: Rodrigo Menezes Date: Wed, 10 Aug 2016 18:48:57 -0700 Subject: [PATCH 144/202] Ensure recycling happens if bag is over capacity. (#3531) Short Description: Ensures you that item Recycling happens if you have more items than the total bag capacity. When you level up, you are awarded items which can cause the bag to be over the capacity. --- pokemongo_bot/cell_workers/recycle_items.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 15c8f7931a..3232870d03 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -28,7 +28,7 @@ def work(self): 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: + 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.", From 00a5b2fa136a5e4b8228743a25bfb2228bda692f Mon Sep 17 00:00:00 2001 From: Amal Samally Date: Thu, 11 Aug 2016 06:44:51 +0400 Subject: [PATCH 145/202] Better inventory: attacks & movesets, IV CP perfection, pokemon level, etc.. (#3455) * Add "level to CP multiplier" data Data is from justinleewells/pogo-optimizer: https://github.com/justinleewells/pogo-optimizer/blob/edd692d/data/game/level-to-cpm.json * Many improvements & additions for the inventory logic - LevelToCPm, FastAttacks, ChargedAttacks, Movesets - More info for each pokemon: attacks data, percent to max cp, IV CP perfection * Add PyCharm/IDEA *.iml (project file) to ignored * Fixes, improvements & refactoring for inventory.py - Return inadvertently deleted pieces of code (thanks to @achretien) - Evolution logic fixes - Other minor fixes - Moveset logic moved to Moveset class * Fix data for pokemons & charged moves * Inventory tests: pokemon data, LevelToCPm, attacks * Fix travis build * Fix info for Hitmonlee & Hitmonchan --- .gitignore | 1 + data/charged_moves.json | 3 +- data/level_to_cpm.json | 81 +++++ data/pokemon.json | 32 +- pokemongo_bot/inventory.py | 676 ++++++++++++++++++++++++++++++++++--- tests/inventory_test.py | 183 ++++++++++ 6 files changed, 907 insertions(+), 69 deletions(-) create mode 100644 data/level_to_cpm.json create mode 100644 tests/inventory_test.py diff --git a/.gitignore b/.gitignore index 4721ce0253..06973c1249 100644 --- a/.gitignore +++ b/.gitignore @@ -100,6 +100,7 @@ share/ # PyCharm IDE settings .idea/ +*.iml # Personal load details src/ diff --git a/data/charged_moves.json b/data/charged_moves.json index 3d487b7201..c3f993191c 100644 --- a/data/charged_moves.json +++ b/data/charged_moves.json @@ -84,9 +84,8 @@ {"id":101,"name":"Flame Charge","type":"Fire","damage":25,"duration":3100,"energy":20,"dps":8.06}, {"id":34,"name":"Heart Stamp","type":"Psychic","damage":20,"duration":2550,"energy":25,"dps":7.84}, {"id":75,"name":"Parabolic Charge","type":"Electric","damage":15,"duration":2100,"energy":20,"dps":7.14}, -{"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":3700,"energy":20,"dps":6.75}, {"id":111,"name":"Icy Wind","type":"Ice","damage":25,"duration":3800,"energy":20,"dps":6.57}, {"id":84,"name":"Disarming Voice","type":"Fairy","damage":25,"duration":3900,"energy":20,"dps":6.41}, {"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":4000,"energy":20,"dps":6.25}, {"id":66,"name":"Shadow Sneak","type":"Ghost","damage":15,"duration":3100,"energy":20,"dps":4.83}, -{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] \ No newline at end of file +{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] diff --git a/data/level_to_cpm.json b/data/level_to_cpm.json new file mode 100644 index 0000000000..d2483d9a41 --- /dev/null +++ b/data/level_to_cpm.json @@ -0,0 +1,81 @@ +{ + "1": 0.094, + "1.5": 0.135137432, + "2": 0.16639787, + "2.5": 0.192650919, + "3": 0.21573247, + "3.5": 0.236572661, + "4": 0.25572005, + "4.5": 0.273530381, + "5": 0.29024988, + "5.5": 0.306057377, + "6": 0.3210876, + "6.5": 0.335445036, + "7": 0.34921268, + "7.5": 0.362457751, + "8": 0.37523559, + "8.5": 0.387592406, + "9": 0.39956728, + "9.5": 0.411193551, + "10": 0.42250001, + "10.5": 0.432926419, + "11": 0.44310755, + "11.5": 0.4530599578, + "12": 0.46279839, + "12.5": 0.472336083, + "13": 0.48168495, + "13.5": 0.4908558, + "14": 0.49985844, + "14.5": 0.508701765, + "15": 0.51739395, + "15.5": 0.525942511, + "16": 0.53435433, + "16.5": 0.542635767, + "17": 0.55079269, + "17.5": 0.558830576, + "18": 0.56675452, + "18.5": 0.574569153, + "19": 0.58227891, + "19.5": 0.589887917, + "20": 0.59740001, + "20.5": 0.604818814, + "21": 0.61215729, + "21.5": 0.619399365, + "22": 0.62656713, + "22.5": 0.633644533, + "23": 0.64065295, + "23.5": 0.647576426, + "24": 0.65443563, + "24.5": 0.661214806, + "25": 0.667934, + "25.5": 0.674577537, + "26": 0.68116492, + "26.5": 0.687680648, + "27": 0.69414365, + "27.5": 0.700538673, + "28": 0.70688421, + "28.5": 0.713164996, + "29": 0.71939909, + "29.5": 0.725571552, + "30": 0.7317, + "30.5": 0.734741009, + "31": 0.73776948, + "31.5": 0.740785574, + "32": 0.74378943, + "32.5": 0.746781211, + "33": 0.74976104, + "33.5": 0.752729087, + "34": 0.75568551, + "34.5": 0.758630378, + "35": 0.76156384, + "35.5": 0.764486065, + "36": 0.76739717, + "36.5": 0.770297266, + "37": 0.7731865, + "37.5": 0.776064962, + "38": 0.77893275, + "38.5": 0.781790055, + "39": 0.78463697, + "39.5": 0.787473578, + "40": 0.79030001 +} \ No newline at end of file diff --git a/data/pokemon.json b/data/pokemon.json index e64b7c31fb..8ccc906d02 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -1232,7 +1232,7 @@ "Previous evolution(s)": [ { "Number": "029", - "Name": "Nidoran F" + "Name": "Nidoran F" } ], "Next Evolution Requirements": { @@ -4326,20 +4326,14 @@ ], "Weight": "49.8 kg", "Height": "1.5 m", - "Next evolution(s)": [ - { - "Number": "107", - "Name": "Hitmonchan" - } - ], "Special Attack(s)": [ "Low Sweep", "Stomp", "Stone Edge" ], "BaseAttack": 148, - "BaseDefense": 100, - "BaseStamina": 172, + "BaseDefense": 172, + "BaseStamina": 100, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -4361,12 +4355,6 @@ ], "Weight": "50.2 kg", "Height": "1.4 m", - "Previous evolution(s)": [ - { - "Number": "106", - "Name": "Hitmonlee" - } - ], "Special Attack(s)": [ "Brick Break", "Fire Punch", @@ -4374,8 +4362,8 @@ "Thunder Punch" ], "BaseAttack": 138, - "BaseDefense": 100, - "BaseStamina": 204, + "BaseDefense": 204, + "BaseStamina": 100, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -5729,6 +5717,16 @@ "Family": 147, "Name": "Dratini candies" }, + "Next evolution(s)": [ + { + "Number": "148", + "Name": "Dragonair" + }, + { + "Number": "149", + "Name": "Dragonite" + } + ], "Special Attack(s)": [ "Aqua Tail", "Twister", diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index ea81b7c093..b27eaa388a 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1,26 +1,45 @@ import json import os + from pokemongo_bot.base_dir import _base_dir ''' 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 + +# +# Abstraction + +class _StaticInventoryComponent(object): STATIC_DATA_FILE = None # optionally load static data from file, # dropping the data in a static variable named STATIC_DATA + STATIC_DATA = None 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)) + cls.STATIC_DATA = cls.process_static_data( + json.load(open(cls.STATIC_DATA_FILE))) + + @classmethod + def process_static_data(cls, data): + # optional hook for processing the static data + # default is to use the data directly + return data + + +class _BaseInventoryComponent(_StaticInventoryComponent): + TYPE = None # base key name for items of this type + ID_FIELD = None # identifier field for items of this type + + def __init__(self): + self._data = {} + super(_BaseInventoryComponent, self).__init__() def parse(self, item): # optional hook for parsing the dict for this item @@ -42,34 +61,22 @@ def retrieve_data(self, inventory): def refresh(self, inventory): self._data = self.retrieve_data(inventory) - def get(self, id): - return self._data.get(id) + def get(self, object_id): + return self._data.get(object_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 +# +# Inventory Components class Candies(_BaseInventoryComponent): TYPE = 'candy' ID_FIELD = 'family_id' @classmethod - def family_id_for(self, pokemon_id): + def family_id_for(cls, pokemon_id): return Pokemons.first_evolution_id_for(pokemon_id) def get(self, pokemon_id): @@ -108,17 +115,120 @@ class Pokemons(_BaseInventoryComponent): ID_FIELD = 'id' STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'pokemon.json') - def parse(self, item): - if 'is_egg' in item: - return Egg(item) - return Pokemon(item) + @classmethod + def process_static_data(cls, data): + pokemon_id = 1 + for poke_info in data: + # prepare types + types = [poke_info['Type I'][0]] # required + for t in poke_info.get('Type II', []): + types.append(t) + poke_info['types'] = types + + # prepare attacks (moves) + cls._process_attacks(poke_info) + cls._process_attacks(poke_info, charged=True) + + # prepare movesets + poke_info['movesets'] = cls._process_movesets(poke_info, pokemon_id) + + # calculate maximum CP for the pokemon (best IVs, lvl 40) + base_attack = poke_info['BaseAttack'] + base_defense = poke_info['BaseDefense'] + base_stamina = poke_info['BaseStamina'] + max_cp = _calc_cp(base_attack, base_defense, base_stamina) + poke_info['max_cp'] = max_cp + + pokemon_id += 1 + return data + + @classmethod + def _process_movesets(cls, poke_info, pokemon_id): + # type: (dict, int) -> List[Moveset] + """ + The optimal moveset is the combination of two moves, one quick move + and one charge move, that deals the most damage over time. + + Because each quick move gains a certain amount of energy (different + for different moves) and each charge move requires a different amount + of energy to use, sometimes, a quick move with lower DPS will be + better since it charges the charge move faster. On the same note, + sometimes a charge move that has lower DPS will be more optimal since + it may require less energy or it may last for a longer period of time. + + Attacker have STAB (Same-type attack bonus - x1.25) pokemon have the + same type as attack. So we add it to the "Combo DPS" of the moveset. + + The defender attacks in intervals of 1 second for the first 2 attacks, + and then in intervals of 2 seconds for the remainder of the attacks. + This explains why we see two consecutive quick attacks at the beginning + of the match. As a result, we add +2 seconds to the DPS calculation + for defender DPS output. + + So to determine an optimal defensive moveset, we follow the same method + as we did for optimal offensive movesets, but instead calculate the + highest "Combo DPS" with an added 2 seconds to the quick move cool down. + + Note: critical hits have not yet been implemented in the game + + See http://pokemongo.gamepress.gg/optimal-moveset-explanation + See http://pokemongo.gamepress.gg/defensive-tactics + """ + + # Prepare movesets + movesets = [] + types = poke_info['types'] + for fm in poke_info['Fast Attack(s)']: + for chm in poke_info['Special Attack(s)']: + movesets.append(Moveset(fm, chm, types, pokemon_id)) + assert len(movesets) > 0 + + # Calculate attack perfection for each moveset + movesets = sorted(movesets, key=lambda m: m.dps_attack) + worst_dps = movesets[0].dps_attack + best_dps = movesets[-1].dps_attack + if best_dps > worst_dps: + for moveset in movesets: + current_dps = moveset.dps_attack + moveset.attack_perfection = \ + (current_dps - worst_dps) / (best_dps - worst_dps) + + # Calculate defense perfection for each moveset + movesets = sorted(movesets, key=lambda m: m.dps_defense) + worst_dps = movesets[0].dps_defense + best_dps = movesets[-1].dps_defense + if best_dps > worst_dps: + for moveset in movesets: + current_dps = moveset.dps_defense + moveset.defense_perfection = \ + (current_dps - worst_dps) / (best_dps - worst_dps) + + return sorted(movesets, key=lambda m: m.dps, reverse=True) + + @classmethod + def _process_attacks(cls, poke_info, charged=False): + # type: (dict, bool) -> List[Attack] + key = 'Fast Attack(s)' if not charged else 'Special Attack(s)' + moves_dict = (ChargedAttacks if charged else FastAttacks).BY_NAME + moves = [] + for name in poke_info[key]: + if name not in moves_dict: + raise KeyError('Unknown {} attack: "{}"'.format( + 'charged' if charged else 'fast', name)) + moves.append(moves_dict[name]) + moves = sorted(moves, key=lambda m: m.dps, reverse=True) + poke_info[key] = moves + assert len(moves) > 0 + return moves @classmethod def data_for(cls, pokemon_id): + # type: (int) -> dict return cls.STATIC_DATA[pokemon_id - 1] @classmethod def name_for(cls, pokemon_id): + # type: (int) -> string return cls.data_for(pokemon_id)['Name'] @classmethod @@ -129,24 +239,194 @@ def first_evolution_id_for(cls, pokemon_id): return pokemon_id @classmethod - def next_evolution_id_for(cls, pokemon_id): + def prev_evolution_id_for(cls, pokemon_id): + data = cls.data_for(pokemon_id) + if 'Previous evolution(s)' in data: + return int(data['Previous evolution(s)'][-1]['Number']) + return None + + @classmethod + def next_evolution_ids_for(cls, pokemon_id): try: - return int(cls.data_for(pokemon_id)['Next evolution(s)'][0]['Number']) + next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] except KeyError: - return None + return [] + # get only next level evolutions, not all possible + ids = [] + for p in next_evolutions: + p_id = int(p['Number']) + if cls.prev_evolution_id_for(p_id) == pokemon_id: + ids.append(p_id) + return ids @classmethod - def evolution_cost_for(cls, pokemon_id): + def last_evolution_ids_for(cls, pokemon_id): try: - return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] except KeyError: - return + return [pokemon_id] + # get only final evolutions, not all possible + ids = [] + for p in next_evolutions: + p_id = int(p['Number']) + if len(cls.data_for(p_id).get('Next evolution(s)', [])) == 0: + ids.append(p_id) + assert len(ids) > 0 + return ids + + @classmethod + def has_next_evolution(cls, pokemon_id): + poke_info = cls.data_for(pokemon_id) + return 'Next Evolution Requirements' in poke_info \ + or 'Next evolution(s)' in poke_info + + @classmethod + def evolution_cost_for(cls, pokemon_id): + if not cls.has_next_evolution(pokemon_id): + return None + return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + + def parse(self, item): + if 'is_egg' in item: + return Egg(item) + return Pokemon(item) 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)] + +# +# Static Components + +class LevelToCPm(_StaticInventoryComponent): + """ + Data for the CP multipliers at different levels + See http://pokemongo.gamepress.gg/cp-multiplier + See https://github.com/justinleewells/pogo-optimizer/blob/edd692d/data/game/level-to-cpm.json + """ + + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'level_to_cpm.json') + MAX_LEVEL = 40 + MAX_CPM = .0 + # half of the lowest difference between CPMs + HALF_DIFF_BETWEEN_HALF_LVL = 14e-3 + + @classmethod + def init_static_data(cls): + super(LevelToCPm, cls).init_static_data() + cls.MAX_CPM = cls.cp_multiplier_for(cls.MAX_LEVEL) + + @classmethod + def cp_multiplier_for(cls, level): + # type: (Union[float, int, string]) -> float + level = float(level) + level = str(int(level) if level.is_integer() else level) + return cls.STATIC_DATA[level] + + @classmethod + def level_from_cpm(cls, cp_multiplier): + # type: (float) -> float + for lvl, cpm in cls.STATIC_DATA.iteritems(): + diff = abs(cpm - cp_multiplier) + if diff <= cls.HALF_DIFF_BETWEEN_HALF_LVL: + return float(lvl) + raise ValueError("Unknown cp_multiplier: {}".format(cp_multiplier)) + + +class _Attacks(_StaticInventoryComponent): + BY_NAME = {} # type: Dict[string, Attack] + BY_TYPE = {} # type: Dict[List[Attack]] + BY_DPS = [] # type: List[Attack] + + @classmethod + def process_static_data(cls, moves): + ret = {} + by_type = {} + by_name = {} + fast = cls is FastAttacks + for attack in moves: + attack = Attack(attack) if fast else ChargedAttack(attack) + ret[attack.id] = attack + by_name[attack.name] = attack + + if attack.type not in by_type: + by_type[attack.type] = [] + by_type[attack.type].append(attack) + + for t in by_type.iterkeys(): + attacks = sorted(by_type[t], key=lambda m: m.dps, reverse=True) + min_dps = attacks[-1].dps + max_dps = attacks[0].dps - min_dps + if max_dps > .0: + for attack in attacks: # type: Attack + attack.rate_in_type = (attack.dps - min_dps) / max_dps + by_type[t] = attacks + + cls.BY_NAME = by_name + cls.BY_TYPE = by_type + cls.BY_DPS = sorted(ret.values(), key=lambda m: m.dps, reverse=True) + + return ret + + @classmethod + def data_for(cls, attack_id): + # type: (int) -> Attack + if attack_id not in cls.STATIC_DATA: + raise ValueError("Attack {} not found in {}".format( + attack_id, cls.__name__)) + return cls.STATIC_DATA[attack_id] + + @classmethod + def by_name(cls, name): + # type: (string) -> Attack + return cls.BY_NAME[name] + + @classmethod + def list_for_type(cls, type_name): + # type: (string) -> List[Attack] + """ + :return: Attacks sorted by DPS in descending order + """ + return cls.BY_TYPE[type_name] + + @classmethod + def all(cls): + return cls.STATIC_DATA.values() + + @classmethod + def all_by_dps(cls): + return cls.BY_DPS + + +class FastAttacks(_Attacks): + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'fast_moves.json') + + +class ChargedAttacks(_Attacks): + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'charged_moves.json') + + +# +# Instances + +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 Egg(object): def __init__(self, data): self._data = data @@ -158,52 +438,297 @@ def has_next_evolution(self): class Pokemon(object): def __init__(self, data): self._data = data + # Unique ID for this particular Pokemon self.id = data['id'] + # Id of the such pokemons in pokedex self.pokemon_id = data['pokemon_id'] + + # Combat points value self.cp = data['cp'] + # Base CP multiplier, fixed at the catch time + self.cp_bm = data['cp_multiplier'] + # Changeable part of the CP multiplier, increasing at power up + self.cp_am = data.get('additional_cp_multiplier', .0) + # Resulting CP multiplier + self.cp_m = self.cp_bm + self.cp_am + + # Current pokemon level (half of level is a normal value) + self.level = LevelToCPm.level_from_cpm(self.cp_m) + + # Maximum health points + self.hp_max = data['stamina_max'] + # Current health points + self.hp = data.get('stamina', self.hp_max) + assert 0 <= self.hp <= self.hp_max + + # Individial Values of the current pokemon (different for each pokemon) + self.iv_attack = data.get('individual_attack', 0) + self.iv_defense = data.get('individual_defense', 0) + self.iv_stamina = data.get('individual_stamina', 0) + self._static_data = Pokemons.data_for(self.pokemon_id) self.name = Pokemons.name_for(self.pokemon_id) - self.iv = self._compute_iv() + self.nickname = data.get('nickname', self.name) + self.in_fort = 'deployed_fort_id' in data self.is_favorite = data.get('favorite', 0) is 1 + # Basic Values of the current pokemon (identical for all such pokemons) + self.base_attack = self._static_data['BaseAttack'] + self.base_defense = self._static_data['BaseDefense'] + self.base_stamina = self._static_data['BaseStamina'] + + # Maximum possible CP for the current pokemon + self.max_cp = self._static_data['max_cp'] + + self.fast_attack = FastAttacks.data_for(data['move_1']) + self.charged_attack = ChargedAttacks.data_for(data['move_2']) + + # Internal values (IV) perfection percent + self.iv = self._compute_iv_perfection() + + # IV CP perfection - kind of IV perfection percent but calculated + # using weight of each IV in its contribution to CP of the best + # evolution of current pokemon + # So it tends to be more accurate than simple IV perfection + self.ivcp = self._compute_cp_perfection() + + # Exact value of current CP (not rounded) + self.cp_exact = _calc_cp( + self.base_attack, self.base_defense, self.base_stamina, + self.iv_attack, self.iv_defense, self.iv_stamina, self.cp_m) + + # Percent of maximum possible CP + self.cp_percent = self.cp_exact / self.max_cp + + # Get moveset instance with calculated DPS and perfection percents + self.moveset = self._get_moveset() + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + def can_evolve_now(self): - return self.has_next_evolution() and self.candy_quantity >= self.evolution_cost + 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 + return Pokemons.has_next_evolution(self.pokemon_id) def has_seen_next_evolution(self): - return pokedex().captured(self.next_evolution_id) + for pokemon_id in self.next_evolution_ids: + if pokedex().captured(pokemon_id): + return True + return False @property - def next_evolution_id(self): - return Pokemons.next_evolution_id_for(self.pokemon_id) + def family_id(self): + return self.first_evolution_id @property def first_evolution_id(self): return Pokemons.first_evolution_id_for(self.pokemon_id) + @property + def prev_evolution_id(self): + return Pokemons.prev_evolution_id_for(self.pokemon_id) + + @property + def next_evolution_ids(self): + return Pokemons.next_evolution_ids_for(self.pokemon_id) + + @property + def last_evolution_ids(self): + return Pokemons.last_evolution_ids_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'] + return Pokemons.evolution_cost_for(self.pokemon_id) + + def _compute_iv_perfection(self): + total_iv = self.iv_attack + self.iv_defense + self.iv_stamina + iv_perfection = round((total_iv / 45.0), 2) + return iv_perfection + + def _compute_cp_perfection(self): + """ + CP perfect percent is more accurate than IV perfect + + We know attack plays an important role in CP, and different + pokemons have different base value, that's means 15/14/15 is + better than 14/15/15 for lot of pokemons, and if one pokemon's + base def is more than base sta, 15/15/14 is better than 15/14/15. + + See https://github.com/jabbink/PokemonGoBot/issues/469 + + So calculate CP perfection at final level for the best of the final + evolutions of the pokemon. + """ + variants = [] + iv_attack = self.iv_attack + iv_defense = self.iv_defense + iv_stamina = self.iv_stamina + cp_m = LevelToCPm.MAX_CPM + last_evolution_ids = self.last_evolution_ids + for pokemon_id in last_evolution_ids: + poke_info = Pokemons.data_for(pokemon_id) + base_attack = poke_info['BaseAttack'] + base_defense = poke_info['BaseDefense'] + base_stamina = poke_info['BaseStamina'] + + # calculate CP variants at maximum level + worst_cp = _calc_cp(base_attack, base_defense, base_stamina, + 0, 0, 0, cp_m) + perfect_cp = _calc_cp(base_attack, base_defense, base_stamina, + cp_multiplier=cp_m) + current_cp = _calc_cp(base_attack, base_defense, base_stamina, + iv_attack, iv_defense, iv_stamina, cp_m) + cp_perfection = (current_cp - worst_cp) / (perfect_cp - worst_cp) + variants.append(cp_perfection) + + # get best value (probably for the best evolution) + cp_perfection = max(variants) + return cp_perfection + + def _get_moveset(self): + move1 = self.fast_attack + move2 = self.charged_attack + movesets = self._static_data['movesets'] + current_moveset = None + for moveset in movesets: # type: Moveset + if moveset.fast_attack == move1 and moveset.charged_attack == move2: + current_moveset = moveset + break + + if current_moveset is None: + raise Exception("Unexpected moveset [{}, {}] for #{} {}".format( + move1, move2, self.pokemon_id, self.name)) + + return current_moveset + + +class Attack(object): + def __init__(self, data): + # self._data = data # Not needed - all saved in fields + self.id = data['id'] + self.name = data['name'] + self.type = data['type'] + self.damage = data['damage'] + self.duration = data['duration'] / 1000.0 # duration in seconds + + # Energy addition for fast attack + # Energy cost for charged attack + self.energy = data['energy'] + + # Damage Per Second + # recalc for better precision + self.dps = self.damage / self.duration + + # Perfection of the attack in it's type (from 0 to 1) + self.rate_in_type = .0 + + @property + def damage_with_stab(self): + # damage with STAB (Same-type attack bonus) + return self.damage * STAB_FACTOR + + @property + def dps_with_stab(self): + # DPS with STAB (Same-type attack bonus) + return self.dps * STAB_FACTOR + + @property + def energy_per_second(self): + return self.energy / self.duration + + @property + def dodge_window(self): + # TODO: Attack Dodge Window + return NotImplemented + + @property + def is_charged(self): + return False + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + + +class ChargedAttack(Attack): + def __init__(self, data): + super(ChargedAttack, self).__init__(data) + + @property + def is_charged(self): + return True + + +class Moveset(object): + def __init__(self, fm, chm, pokemon_types=(), pokemon_id=-1): + # type: (Attack, ChargedAttack, List[string], int) -> None + self.pokemon_id = pokemon_id + self.fast_attack = fm + self.charged_attack = chm + + # See Pokemons._process_movesets() + # See http://pokemongo.gamepress.gg/optimal-moveset-explanation + # See http://pokemongo.gamepress.gg/defensive-tactics + + fm_number = 100 # for simplicity we use 100 + + fm_energy = fm.energy * fm_number + fm_damage = fm.damage * fm_number + fm_secs = fm.duration * fm_number + + # Defender attacks in intervals of 1 second for the + # first 2 attacks, and then in intervals of 2 seconds + # So add 1.95 seconds to the quick move cool down for defense + # 1.95 is something like an average here + # TODO: Do something better? + fm_defense_secs = (fm.duration + 1.95) * fm_number + + chm_number = fm_energy / chm.energy + chm_damage = chm.damage * chm_number + chm_secs = chm.duration * chm_number + + damage_sum = fm_damage + chm_damage + # raw Damage-Per-Second for the moveset + self.dps = damage_sum / (fm_secs + chm_secs) + # average DPS for defense + self.dps_defense = damage_sum / (fm_defense_secs + chm_secs) + + # apply STAB (Same-type attack bonus) + if fm.type in pokemon_types: + fm_damage *= STAB_FACTOR + if chm.type in pokemon_types: + chm_damage *= STAB_FACTOR + + # DPS for attack (counting STAB) + self.dps_attack = (fm_damage + chm_damage) / (fm_secs + chm_secs) - def _compute_iv(self): - total_IV = 0.0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] + # Moveset perfection percent attack and for defense + # Calculated for current pokemon, not between all pokemons + # So 100% perfect moveset can be weak if pokemon is weak (e.g. Caterpie) + self.attack_perfection = .0 + self.defense_perfection = .0 - 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 + # TODO: True DPS for real combat (floor(Attack/200 * MovePower * STAB) + 1) + # See http://pokemongo.gamepress.gg/pokemon-attack-explanation + + def __str__(self): + return '[{}, {}]'.format(self.fast_attack, self.charged_attack) + + def __repr__(self): + return '[{}, {}]'.format(self.fast_attack, self.charged_attack) class Inventory(object): @@ -227,8 +752,47 @@ def refresh(self): with open(user_web_inventory, 'w') as outfile: json.dump(inventory, outfile) +# +# Usage helpers + +# STAB (Same-type attack bonus) +STAB_FACTOR = 1.25 _inventory = None +LevelToCPm() # init LevelToCPm +FastAttacks() # init FastAttacks +ChargedAttacks() # init ChargedAttacks + + +def _calc_cp(base_attack, base_defense, base_stamina, + iv_attack=15, iv_defense=15, iv_stamina=15, + cp_multiplier=LevelToCPm.MAX_CPM): + """ + CP calculation + + CP = (Attack * Defense^0.5 * Stamina^0.5 * CP_Multiplier^2) / 10 + CP = (BaseAtk+AtkIV) * (BaseDef+DefIV)^0.5 * (BaseStam+StamIV)^0.5 * Lvl(CPScalar)^2 / 10 + + See https://www.reddit.com/r/TheSilphRoad/comments/4t7r4d/exact_pokemon_cp_formula/ + See https://www.reddit.com/r/pokemongodev/comments/4t7xb4/exact_cp_formula_from_stats_and_cpm_and_an_update/ + See http://pokemongo.gamepress.gg/pokemon-stats-advanced + See http://pokemongo.gamepress.gg/cp-multiplier + See http://gaming.stackexchange.com/questions/280491/formula-to-calculate-pokemon-go-cp-and-hp + + :param base_attack: Pokemon BaseAttack + :param base_defense: Pokemon BaseDefense + :param base_stamina: Pokemon BaseStamina + :param iv_attack: Pokemon IndividualAttack (0..15) + :param iv_defense: Pokemon IndividualDefense (0..15) + :param iv_stamina: Pokemon IndividualStamina (0..15) + :param cp_multiplier: CP Multiplier (0.79030001 is max - value for level 40) + :return: CP as float + """ + return (base_attack + iv_attack) \ + * ((base_defense + iv_defense)**0.5) \ + * ((base_stamina + iv_stamina)**0.5) \ + * (cp_multiplier ** 2) / 10 + def init_inventory(bot): global _inventory @@ -257,3 +821,15 @@ def pokemons(refresh=False): def items(): return _inventory.items + + +def levels_to_cpm(): + return LevelToCPm + + +def fast_attacks(): + return FastAttacks + + +def charged_attacks(): + return ChargedAttacks diff --git a/tests/inventory_test.py b/tests/inventory_test.py new file mode 100644 index 0000000000..8362ce2b91 --- /dev/null +++ b/tests/inventory_test.py @@ -0,0 +1,183 @@ +import unittest + +from pokemongo_bot.inventory import * + + +class InventoryTest(unittest.TestCase): + def test_pokemons(self): + # Init data + self.assertEqual(len(Pokemons().all()), 0) # No inventory loaded here + + obj = Pokemons + self.assertEqual(len(obj.STATIC_DATA), 151) + + for poke_info in obj.STATIC_DATA: + name = poke_info['Name'] + pokemon_id = int(poke_info['Number']) + self.assertTrue(1 <= pokemon_id <= 151) + + self.assertGreaterEqual(len(poke_info['movesets']), 1) + self.assertTrue(262 <= poke_info['max_cp'] <= 4145) + self.assertTrue(1 <= len(poke_info['types']) <= 2) + self.assertTrue(40 <= poke_info['BaseAttack'] <= 284) + self.assertTrue(20 <= poke_info['BaseDefense'] <= 500) + self.assertTrue(54 <= poke_info['BaseStamina'] <= 242) + self.assertTrue(.0 <= poke_info['CaptureRate'] <= .56) + self.assertTrue(.0 <= poke_info['FleeRate'] <= .99) + self.assertTrue(1 <= len(poke_info['Weaknesses']) <= 7) + self.assertTrue(3 <= len(name) <= 10) + + self.assertGreaterEqual(len(poke_info['Classification']), 11) + self.assertGreaterEqual(len(poke_info['Fast Attack(s)']), 1) + self.assertGreaterEqual(len(poke_info['Special Attack(s)']), 1) + + self.assertIs(obj.data_for(pokemon_id), poke_info) + self.assertIs(obj.name_for(pokemon_id), name) + + first_evolution_id = obj.first_evolution_id_for(pokemon_id) + self.assertGreaterEqual(first_evolution_id, 1) + next_evolution_ids = obj.next_evolution_ids_for(pokemon_id) + last_evolution_ids = obj.last_evolution_ids_for(pokemon_id) + candies_cost = obj.evolution_cost_for(pokemon_id) + obj.prev_evolution_id_for(pokemon_id) # just call test + self.assertGreaterEqual(len(last_evolution_ids), 1) + + if not obj.has_next_evolution(pokemon_id): + assert 'Next evolution(s)' not in poke_info + assert 'Next Evolution Requirements' not in poke_info + else: + self.assertGreaterEqual(len(next_evolution_ids), 1) + self.assertLessEqual(len(next_evolution_ids), len(last_evolution_ids)) + + reqs = poke_info['Next Evolution Requirements'] + self.assertEqual(reqs["Family"], first_evolution_id) + candies_name = obj.name_for(first_evolution_id) + ' candies' + self.assertEqual(reqs["Name"], candies_name) + self.assertIsNotNone(candies_cost) + self.assertTrue(12 <= candies_cost <= 400) + self.assertEqual(reqs["Amount"], candies_cost) + + evolutions = poke_info["Next evolution(s)"] + self.assertGreaterEqual(len(evolutions), len(next_evolution_ids)) + + for p in evolutions: + p_id = int(p["Number"]) + self.assertNotEqual(p_id, pokemon_id) + self.assertEqual(p["Name"], obj.name_for(p_id)) + + for p_id in next_evolution_ids: + self.assertEqual(obj.prev_evolution_id_for(p_id), pokemon_id) + prev_evs = obj.data_for(p_id)["Previous evolution(s)"] + self.assertGreaterEqual(len(prev_evs), 1) + self.assertEqual(int(prev_evs[-1]["Number"]), pokemon_id) + self.assertEqual(prev_evs[-1]["Name"], name) + + # Only Eevee has 3 next evolutions + self.assertEqual(len(next_evolution_ids), + 1 if pokemon_id != 133 else 3) + + if "Previous evolution(s)" in poke_info: + for p in poke_info["Previous evolution(s)"]: + p_id = int(p["Number"]) + self.assertNotEqual(p_id, pokemon_id) + self.assertEqual(p["Name"], obj.name_for(p_id)) + + # + # Specific pokemons testing + + poke = Pokemon({ + "num_upgrades": 2, "move_1": 210, "move_2": 69, "pokeball": 2, + "favorite": 1, "pokemon_id": 42, "battles_attacked": 4, + "stamina": 76, "stamina_max": 76, "individual_attack": 9, + "individual_defense": 4, "individual_stamina": 8, + "cp_multiplier": 0.4627983868122101, + "additional_cp_multiplier": 0.018886566162109375, + "cp": 653, "nickname": "Golb", "id": 13632861873471324}) + self.assertEqual(poke.level, 12.5) + self.assertEqual(poke.iv, 0.47) + self.assertAlmostEqual(poke.ivcp, 0.482845351) + self.assertAlmostEqual(poke.max_cp, 1921.34561459) + self.assertAlmostEqual(poke.cp_percent, 0.34000973) + self.assertTrue(poke.is_favorite) + self.assertEqual(poke.name, 'Golbat') + self.assertEqual(poke.nickname, "Golb") + self.assertAlmostEqual(poke.moveset.dps, 10.7540173053) + self.assertAlmostEqual(poke.moveset.dps_attack, 12.14462299) + self.assertAlmostEqual(poke.moveset.dps_defense, 4.876681614) + self.assertAlmostEqual(poke.moveset.attack_perfection, 0.4720730048) + self.assertAlmostEqual(poke.moveset.defense_perfection, 0.8158081497) + + poke = Pokemon({ + "move_1": 221, "move_2": 129, "pokemon_id": 19, "cp": 110, + "individual_attack": 6, "stamina_max": 22, "individual_defense": 14, + "cp_multiplier": 0.37523558735847473, "id": 7841053399}) + self.assertEqual(poke.level, 7.5) + self.assertEqual(poke.iv, 0.44) + self.assertAlmostEqual(poke.ivcp, 0.452398293) + self.assertAlmostEqual(poke.max_cp, 581.64643575) + self.assertAlmostEqual(poke.cp_percent, 0.189251848608) + self.assertFalse(poke.is_favorite) + self.assertEqual(poke.name, 'Rattata') + self.assertEqual(poke.nickname, 'Rattata') + self.assertAlmostEqual(poke.moveset.dps, 12.5567813108) + self.assertAlmostEqual(poke.moveset.dps_attack, 15.6959766385) + self.assertAlmostEqual(poke.moveset.dps_defense, 5.54282440561) + self.assertAlmostEqual(poke.moveset.attack_perfection, 0.835172881385) + self.assertAlmostEqual(poke.moveset.defense_perfection, 0.603137650999) + + def test_levels_to_cpm(self): + l2c = LevelToCPm + self.assertIs(levels_to_cpm(), l2c) + max_cpm = l2c.cp_multiplier_for(l2c.MAX_LEVEL) + self.assertEqual(l2c.MAX_LEVEL, 40) + self.assertEqual(l2c.MAX_CPM, max_cpm) + self.assertEqual(len(l2c.STATIC_DATA), 79) + + self.assertEqual(l2c.cp_multiplier_for("1"), 0.094) + self.assertEqual(l2c.cp_multiplier_for(1), 0.094) + self.assertEqual(l2c.cp_multiplier_for(1.0), 0.094) + self.assertEqual(l2c.cp_multiplier_for("17.5"), 0.558830576) + self.assertEqual(l2c.cp_multiplier_for(17.5), 0.558830576) + self.assertEqual(l2c.cp_multiplier_for('40.0'), 0.79030001) + self.assertEqual(l2c.cp_multiplier_for(40.0), 0.79030001) + self.assertEqual(l2c.cp_multiplier_for(40), 0.79030001) + + self.assertEqual(l2c.level_from_cpm(0.79030001), 40.0) + self.assertEqual(l2c.level_from_cpm(0.7903), 40.0) + + def test_attacks(self): + self._test_attacks(fast_attacks, FastAttacks) + self._test_attacks(charged_attacks, ChargedAttacks) + + def _test_attacks(self, callback, clazz): + charged = clazz is ChargedAttacks + self.assertIs(callback(), clazz) + + # check consistency + attacks = clazz.all_by_dps() + number = len(attacks) + self.assertTrue(number > 0) + self.assertGreaterEqual(len(clazz.BY_TYPE), 17) + self.assertEqual(number, len(clazz.all())) + self.assertEqual(number, len(clazz.STATIC_DATA)) + self.assertEqual(number, len(clazz.BY_NAME)) + self.assertEqual(number, sum([len(l) for l in clazz.BY_TYPE.values()])) + + # check data + prev_dps = float("inf") + for attack in attacks: # type: Attack + self.assertGreater(attack.id, 0) + self.assertGreater(len(attack.name), 0) + self.assertGreater(len(attack.type), 0) + self.assertGreaterEqual(attack.damage, 0) + self.assertGreater(attack.duration, .0) + self.assertGreater(attack.energy, 0) + self.assertGreaterEqual(attack.dps, 0) + self.assertTrue(.0 <= attack.rate_in_type <= 1.0) + self.assertLessEqual(attack.dps, prev_dps) + self.assertEqual(attack.is_charged, charged) + self.assertIs(attack, clazz.data_for(attack.id)) + self.assertIs(attack, clazz.by_name(attack.name)) + self.assertTrue(attack in clazz.BY_TYPE[attack.type]) + self.assertIsInstance(attack, ChargedAttack if charged else Attack) + prev_dps = attack.dps From e9b229ec0fd14a4814ea7431b1256850e907cfbf Mon Sep 17 00:00:00 2001 From: Nikhil Pandey Date: Thu, 11 Aug 2016 09:34:11 +0545 Subject: [PATCH 146/202] Revert "Better inventory: attacks & movesets, IV CP perfection, pokemon level, etc.." (#3549) --- .gitignore | 1 - data/charged_moves.json | 3 +- data/level_to_cpm.json | 81 ----- data/pokemon.json | 32 +- pokemongo_bot/inventory.py | 676 +++---------------------------------- tests/inventory_test.py | 183 ---------- 6 files changed, 69 insertions(+), 907 deletions(-) delete mode 100644 data/level_to_cpm.json delete mode 100644 tests/inventory_test.py diff --git a/.gitignore b/.gitignore index 06973c1249..4721ce0253 100644 --- a/.gitignore +++ b/.gitignore @@ -100,7 +100,6 @@ share/ # PyCharm IDE settings .idea/ -*.iml # Personal load details src/ diff --git a/data/charged_moves.json b/data/charged_moves.json index c3f993191c..3d487b7201 100644 --- a/data/charged_moves.json +++ b/data/charged_moves.json @@ -84,8 +84,9 @@ {"id":101,"name":"Flame Charge","type":"Fire","damage":25,"duration":3100,"energy":20,"dps":8.06}, {"id":34,"name":"Heart Stamp","type":"Psychic","damage":20,"duration":2550,"energy":25,"dps":7.84}, {"id":75,"name":"Parabolic Charge","type":"Electric","damage":15,"duration":2100,"energy":20,"dps":7.14}, +{"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":3700,"energy":20,"dps":6.75}, {"id":111,"name":"Icy Wind","type":"Ice","damage":25,"duration":3800,"energy":20,"dps":6.57}, {"id":84,"name":"Disarming Voice","type":"Fairy","damage":25,"duration":3900,"energy":20,"dps":6.41}, {"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":4000,"energy":20,"dps":6.25}, {"id":66,"name":"Shadow Sneak","type":"Ghost","damage":15,"duration":3100,"energy":20,"dps":4.83}, -{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] +{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] \ No newline at end of file diff --git a/data/level_to_cpm.json b/data/level_to_cpm.json deleted file mode 100644 index d2483d9a41..0000000000 --- a/data/level_to_cpm.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "1": 0.094, - "1.5": 0.135137432, - "2": 0.16639787, - "2.5": 0.192650919, - "3": 0.21573247, - "3.5": 0.236572661, - "4": 0.25572005, - "4.5": 0.273530381, - "5": 0.29024988, - "5.5": 0.306057377, - "6": 0.3210876, - "6.5": 0.335445036, - "7": 0.34921268, - "7.5": 0.362457751, - "8": 0.37523559, - "8.5": 0.387592406, - "9": 0.39956728, - "9.5": 0.411193551, - "10": 0.42250001, - "10.5": 0.432926419, - "11": 0.44310755, - "11.5": 0.4530599578, - "12": 0.46279839, - "12.5": 0.472336083, - "13": 0.48168495, - "13.5": 0.4908558, - "14": 0.49985844, - "14.5": 0.508701765, - "15": 0.51739395, - "15.5": 0.525942511, - "16": 0.53435433, - "16.5": 0.542635767, - "17": 0.55079269, - "17.5": 0.558830576, - "18": 0.56675452, - "18.5": 0.574569153, - "19": 0.58227891, - "19.5": 0.589887917, - "20": 0.59740001, - "20.5": 0.604818814, - "21": 0.61215729, - "21.5": 0.619399365, - "22": 0.62656713, - "22.5": 0.633644533, - "23": 0.64065295, - "23.5": 0.647576426, - "24": 0.65443563, - "24.5": 0.661214806, - "25": 0.667934, - "25.5": 0.674577537, - "26": 0.68116492, - "26.5": 0.687680648, - "27": 0.69414365, - "27.5": 0.700538673, - "28": 0.70688421, - "28.5": 0.713164996, - "29": 0.71939909, - "29.5": 0.725571552, - "30": 0.7317, - "30.5": 0.734741009, - "31": 0.73776948, - "31.5": 0.740785574, - "32": 0.74378943, - "32.5": 0.746781211, - "33": 0.74976104, - "33.5": 0.752729087, - "34": 0.75568551, - "34.5": 0.758630378, - "35": 0.76156384, - "35.5": 0.764486065, - "36": 0.76739717, - "36.5": 0.770297266, - "37": 0.7731865, - "37.5": 0.776064962, - "38": 0.77893275, - "38.5": 0.781790055, - "39": 0.78463697, - "39.5": 0.787473578, - "40": 0.79030001 -} \ No newline at end of file diff --git a/data/pokemon.json b/data/pokemon.json index 8ccc906d02..e64b7c31fb 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -1232,7 +1232,7 @@ "Previous evolution(s)": [ { "Number": "029", - "Name": "Nidoran F" + "Name": "Nidoran F" } ], "Next Evolution Requirements": { @@ -4326,14 +4326,20 @@ ], "Weight": "49.8 kg", "Height": "1.5 m", + "Next evolution(s)": [ + { + "Number": "107", + "Name": "Hitmonchan" + } + ], "Special Attack(s)": [ "Low Sweep", "Stomp", "Stone Edge" ], "BaseAttack": 148, - "BaseDefense": 172, - "BaseStamina": 100, + "BaseDefense": 100, + "BaseStamina": 172, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -4355,6 +4361,12 @@ ], "Weight": "50.2 kg", "Height": "1.4 m", + "Previous evolution(s)": [ + { + "Number": "106", + "Name": "Hitmonlee" + } + ], "Special Attack(s)": [ "Brick Break", "Fire Punch", @@ -4362,8 +4374,8 @@ "Thunder Punch" ], "BaseAttack": 138, - "BaseDefense": 204, - "BaseStamina": 100, + "BaseDefense": 100, + "BaseStamina": 204, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -5717,16 +5729,6 @@ "Family": 147, "Name": "Dratini candies" }, - "Next evolution(s)": [ - { - "Number": "148", - "Name": "Dragonair" - }, - { - "Number": "149", - "Name": "Dragonite" - } - ], "Special Attack(s)": [ "Aqua Tail", "Twister", diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index b27eaa388a..ea81b7c093 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1,45 +1,26 @@ import json import os - from pokemongo_bot.base_dir import _base_dir ''' Helper class for updating/retrieving Inventory data ''' - -# -# Abstraction - -class _StaticInventoryComponent(object): +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 - STATIC_DATA = None 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 = cls.process_static_data( - json.load(open(cls.STATIC_DATA_FILE))) - - @classmethod - def process_static_data(cls, data): - # optional hook for processing the static data - # default is to use the data directly - return data - - -class _BaseInventoryComponent(_StaticInventoryComponent): - TYPE = None # base key name for items of this type - ID_FIELD = None # identifier field for items of this type - - def __init__(self): - self._data = {} - super(_BaseInventoryComponent, self).__init__() + cls.STATIC_DATA = json.load(open(cls.STATIC_DATA_FILE)) def parse(self, item): # optional hook for parsing the dict for this item @@ -61,22 +42,34 @@ def retrieve_data(self, inventory): def refresh(self, inventory): self._data = self.retrieve_data(inventory) - def get(self, object_id): - return self._data.get(object_id) + def get(self, id): + return self._data.get(id) def all(self): return list(self._data.values()) -# -# Inventory Components +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(cls, pokemon_id): + def family_id_for(self, pokemon_id): return Pokemons.first_evolution_id_for(pokemon_id) def get(self, pokemon_id): @@ -115,120 +108,17 @@ class Pokemons(_BaseInventoryComponent): ID_FIELD = 'id' STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'pokemon.json') - @classmethod - def process_static_data(cls, data): - pokemon_id = 1 - for poke_info in data: - # prepare types - types = [poke_info['Type I'][0]] # required - for t in poke_info.get('Type II', []): - types.append(t) - poke_info['types'] = types - - # prepare attacks (moves) - cls._process_attacks(poke_info) - cls._process_attacks(poke_info, charged=True) - - # prepare movesets - poke_info['movesets'] = cls._process_movesets(poke_info, pokemon_id) - - # calculate maximum CP for the pokemon (best IVs, lvl 40) - base_attack = poke_info['BaseAttack'] - base_defense = poke_info['BaseDefense'] - base_stamina = poke_info['BaseStamina'] - max_cp = _calc_cp(base_attack, base_defense, base_stamina) - poke_info['max_cp'] = max_cp - - pokemon_id += 1 - return data - - @classmethod - def _process_movesets(cls, poke_info, pokemon_id): - # type: (dict, int) -> List[Moveset] - """ - The optimal moveset is the combination of two moves, one quick move - and one charge move, that deals the most damage over time. - - Because each quick move gains a certain amount of energy (different - for different moves) and each charge move requires a different amount - of energy to use, sometimes, a quick move with lower DPS will be - better since it charges the charge move faster. On the same note, - sometimes a charge move that has lower DPS will be more optimal since - it may require less energy or it may last for a longer period of time. - - Attacker have STAB (Same-type attack bonus - x1.25) pokemon have the - same type as attack. So we add it to the "Combo DPS" of the moveset. - - The defender attacks in intervals of 1 second for the first 2 attacks, - and then in intervals of 2 seconds for the remainder of the attacks. - This explains why we see two consecutive quick attacks at the beginning - of the match. As a result, we add +2 seconds to the DPS calculation - for defender DPS output. - - So to determine an optimal defensive moveset, we follow the same method - as we did for optimal offensive movesets, but instead calculate the - highest "Combo DPS" with an added 2 seconds to the quick move cool down. - - Note: critical hits have not yet been implemented in the game - - See http://pokemongo.gamepress.gg/optimal-moveset-explanation - See http://pokemongo.gamepress.gg/defensive-tactics - """ - - # Prepare movesets - movesets = [] - types = poke_info['types'] - for fm in poke_info['Fast Attack(s)']: - for chm in poke_info['Special Attack(s)']: - movesets.append(Moveset(fm, chm, types, pokemon_id)) - assert len(movesets) > 0 - - # Calculate attack perfection for each moveset - movesets = sorted(movesets, key=lambda m: m.dps_attack) - worst_dps = movesets[0].dps_attack - best_dps = movesets[-1].dps_attack - if best_dps > worst_dps: - for moveset in movesets: - current_dps = moveset.dps_attack - moveset.attack_perfection = \ - (current_dps - worst_dps) / (best_dps - worst_dps) - - # Calculate defense perfection for each moveset - movesets = sorted(movesets, key=lambda m: m.dps_defense) - worst_dps = movesets[0].dps_defense - best_dps = movesets[-1].dps_defense - if best_dps > worst_dps: - for moveset in movesets: - current_dps = moveset.dps_defense - moveset.defense_perfection = \ - (current_dps - worst_dps) / (best_dps - worst_dps) - - return sorted(movesets, key=lambda m: m.dps, reverse=True) - - @classmethod - def _process_attacks(cls, poke_info, charged=False): - # type: (dict, bool) -> List[Attack] - key = 'Fast Attack(s)' if not charged else 'Special Attack(s)' - moves_dict = (ChargedAttacks if charged else FastAttacks).BY_NAME - moves = [] - for name in poke_info[key]: - if name not in moves_dict: - raise KeyError('Unknown {} attack: "{}"'.format( - 'charged' if charged else 'fast', name)) - moves.append(moves_dict[name]) - moves = sorted(moves, key=lambda m: m.dps, reverse=True) - poke_info[key] = moves - assert len(moves) > 0 - return moves + def parse(self, item): + if 'is_egg' in item: + return Egg(item) + return Pokemon(item) @classmethod def data_for(cls, pokemon_id): - # type: (int) -> dict return cls.STATIC_DATA[pokemon_id - 1] @classmethod def name_for(cls, pokemon_id): - # type: (int) -> string return cls.data_for(pokemon_id)['Name'] @classmethod @@ -239,194 +129,24 @@ def first_evolution_id_for(cls, pokemon_id): return pokemon_id @classmethod - def prev_evolution_id_for(cls, pokemon_id): - data = cls.data_for(pokemon_id) - if 'Previous evolution(s)' in data: - return int(data['Previous evolution(s)'][-1]['Number']) - return None - - @classmethod - def next_evolution_ids_for(cls, pokemon_id): + def next_evolution_id_for(cls, pokemon_id): try: - next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] + return int(cls.data_for(pokemon_id)['Next evolution(s)'][0]['Number']) except KeyError: - return [] - # get only next level evolutions, not all possible - ids = [] - for p in next_evolutions: - p_id = int(p['Number']) - if cls.prev_evolution_id_for(p_id) == pokemon_id: - ids.append(p_id) - return ids + return None @classmethod - def last_evolution_ids_for(cls, pokemon_id): + def evolution_cost_for(cls, pokemon_id): try: - next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] + return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) except KeyError: - return [pokemon_id] - # get only final evolutions, not all possible - ids = [] - for p in next_evolutions: - p_id = int(p['Number']) - if len(cls.data_for(p_id).get('Next evolution(s)', [])) == 0: - ids.append(p_id) - assert len(ids) > 0 - return ids - - @classmethod - def has_next_evolution(cls, pokemon_id): - poke_info = cls.data_for(pokemon_id) - return 'Next Evolution Requirements' in poke_info \ - or 'Next evolution(s)' in poke_info - - @classmethod - def evolution_cost_for(cls, pokemon_id): - if not cls.has_next_evolution(pokemon_id): - return None - return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) - - def parse(self, item): - if 'is_egg' in item: - return Egg(item) - return Pokemon(item) + 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)] - -# -# Static Components - -class LevelToCPm(_StaticInventoryComponent): - """ - Data for the CP multipliers at different levels - See http://pokemongo.gamepress.gg/cp-multiplier - See https://github.com/justinleewells/pogo-optimizer/blob/edd692d/data/game/level-to-cpm.json - """ - - STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'level_to_cpm.json') - MAX_LEVEL = 40 - MAX_CPM = .0 - # half of the lowest difference between CPMs - HALF_DIFF_BETWEEN_HALF_LVL = 14e-3 - - @classmethod - def init_static_data(cls): - super(LevelToCPm, cls).init_static_data() - cls.MAX_CPM = cls.cp_multiplier_for(cls.MAX_LEVEL) - - @classmethod - def cp_multiplier_for(cls, level): - # type: (Union[float, int, string]) -> float - level = float(level) - level = str(int(level) if level.is_integer() else level) - return cls.STATIC_DATA[level] - - @classmethod - def level_from_cpm(cls, cp_multiplier): - # type: (float) -> float - for lvl, cpm in cls.STATIC_DATA.iteritems(): - diff = abs(cpm - cp_multiplier) - if diff <= cls.HALF_DIFF_BETWEEN_HALF_LVL: - return float(lvl) - raise ValueError("Unknown cp_multiplier: {}".format(cp_multiplier)) - - -class _Attacks(_StaticInventoryComponent): - BY_NAME = {} # type: Dict[string, Attack] - BY_TYPE = {} # type: Dict[List[Attack]] - BY_DPS = [] # type: List[Attack] - - @classmethod - def process_static_data(cls, moves): - ret = {} - by_type = {} - by_name = {} - fast = cls is FastAttacks - for attack in moves: - attack = Attack(attack) if fast else ChargedAttack(attack) - ret[attack.id] = attack - by_name[attack.name] = attack - - if attack.type not in by_type: - by_type[attack.type] = [] - by_type[attack.type].append(attack) - - for t in by_type.iterkeys(): - attacks = sorted(by_type[t], key=lambda m: m.dps, reverse=True) - min_dps = attacks[-1].dps - max_dps = attacks[0].dps - min_dps - if max_dps > .0: - for attack in attacks: # type: Attack - attack.rate_in_type = (attack.dps - min_dps) / max_dps - by_type[t] = attacks - - cls.BY_NAME = by_name - cls.BY_TYPE = by_type - cls.BY_DPS = sorted(ret.values(), key=lambda m: m.dps, reverse=True) - - return ret - - @classmethod - def data_for(cls, attack_id): - # type: (int) -> Attack - if attack_id not in cls.STATIC_DATA: - raise ValueError("Attack {} not found in {}".format( - attack_id, cls.__name__)) - return cls.STATIC_DATA[attack_id] - - @classmethod - def by_name(cls, name): - # type: (string) -> Attack - return cls.BY_NAME[name] - - @classmethod - def list_for_type(cls, type_name): - # type: (string) -> List[Attack] - """ - :return: Attacks sorted by DPS in descending order - """ - return cls.BY_TYPE[type_name] - - @classmethod - def all(cls): - return cls.STATIC_DATA.values() - - @classmethod - def all_by_dps(cls): - return cls.BY_DPS - - -class FastAttacks(_Attacks): - STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'fast_moves.json') - - -class ChargedAttacks(_Attacks): - STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'charged_moves.json') - - -# -# Instances - -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 Egg(object): def __init__(self, data): self._data = data @@ -438,297 +158,52 @@ def has_next_evolution(self): class Pokemon(object): def __init__(self, data): self._data = data - # Unique ID for this particular Pokemon self.id = data['id'] - # Id of the such pokemons in pokedex self.pokemon_id = data['pokemon_id'] - - # Combat points value self.cp = data['cp'] - # Base CP multiplier, fixed at the catch time - self.cp_bm = data['cp_multiplier'] - # Changeable part of the CP multiplier, increasing at power up - self.cp_am = data.get('additional_cp_multiplier', .0) - # Resulting CP multiplier - self.cp_m = self.cp_bm + self.cp_am - - # Current pokemon level (half of level is a normal value) - self.level = LevelToCPm.level_from_cpm(self.cp_m) - - # Maximum health points - self.hp_max = data['stamina_max'] - # Current health points - self.hp = data.get('stamina', self.hp_max) - assert 0 <= self.hp <= self.hp_max - - # Individial Values of the current pokemon (different for each pokemon) - self.iv_attack = data.get('individual_attack', 0) - self.iv_defense = data.get('individual_defense', 0) - self.iv_stamina = data.get('individual_stamina', 0) - self._static_data = Pokemons.data_for(self.pokemon_id) self.name = Pokemons.name_for(self.pokemon_id) - self.nickname = data.get('nickname', self.name) - + self.iv = self._compute_iv() self.in_fort = 'deployed_fort_id' in data self.is_favorite = data.get('favorite', 0) is 1 - # Basic Values of the current pokemon (identical for all such pokemons) - self.base_attack = self._static_data['BaseAttack'] - self.base_defense = self._static_data['BaseDefense'] - self.base_stamina = self._static_data['BaseStamina'] - - # Maximum possible CP for the current pokemon - self.max_cp = self._static_data['max_cp'] - - self.fast_attack = FastAttacks.data_for(data['move_1']) - self.charged_attack = ChargedAttacks.data_for(data['move_2']) - - # Internal values (IV) perfection percent - self.iv = self._compute_iv_perfection() - - # IV CP perfection - kind of IV perfection percent but calculated - # using weight of each IV in its contribution to CP of the best - # evolution of current pokemon - # So it tends to be more accurate than simple IV perfection - self.ivcp = self._compute_cp_perfection() - - # Exact value of current CP (not rounded) - self.cp_exact = _calc_cp( - self.base_attack, self.base_defense, self.base_stamina, - self.iv_attack, self.iv_defense, self.iv_stamina, self.cp_m) - - # Percent of maximum possible CP - self.cp_percent = self.cp_exact / self.max_cp - - # Get moveset instance with calculated DPS and perfection percents - self.moveset = self._get_moveset() - - def __str__(self): - return self.name - - def __repr__(self): - return self.name - def can_evolve_now(self): - return self.has_next_evolution() and \ - self.candy_quantity >= self.evolution_cost + return self.has_next_evolution() and self.candy_quantity >= self.evolution_cost def has_next_evolution(self): - return Pokemons.has_next_evolution(self.pokemon_id) + return 'Next Evolution Requirements' in self._static_data def has_seen_next_evolution(self): - for pokemon_id in self.next_evolution_ids: - if pokedex().captured(pokemon_id): - return True - return False + return pokedex().captured(self.next_evolution_id) @property - def family_id(self): - return self.first_evolution_id + 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 prev_evolution_id(self): - return Pokemons.prev_evolution_id_for(self.pokemon_id) - - @property - def next_evolution_ids(self): - return Pokemons.next_evolution_ids_for(self.pokemon_id) - - @property - def last_evolution_ids(self): - return Pokemons.last_evolution_ids_for(self.pokemon_id) - @property def candy_quantity(self): return candies().get(self.pokemon_id).quantity @property def evolution_cost(self): - return Pokemons.evolution_cost_for(self.pokemon_id) - - def _compute_iv_perfection(self): - total_iv = self.iv_attack + self.iv_defense + self.iv_stamina - iv_perfection = round((total_iv / 45.0), 2) - return iv_perfection - - def _compute_cp_perfection(self): - """ - CP perfect percent is more accurate than IV perfect - - We know attack plays an important role in CP, and different - pokemons have different base value, that's means 15/14/15 is - better than 14/15/15 for lot of pokemons, and if one pokemon's - base def is more than base sta, 15/15/14 is better than 15/14/15. - - See https://github.com/jabbink/PokemonGoBot/issues/469 - - So calculate CP perfection at final level for the best of the final - evolutions of the pokemon. - """ - variants = [] - iv_attack = self.iv_attack - iv_defense = self.iv_defense - iv_stamina = self.iv_stamina - cp_m = LevelToCPm.MAX_CPM - last_evolution_ids = self.last_evolution_ids - for pokemon_id in last_evolution_ids: - poke_info = Pokemons.data_for(pokemon_id) - base_attack = poke_info['BaseAttack'] - base_defense = poke_info['BaseDefense'] - base_stamina = poke_info['BaseStamina'] - - # calculate CP variants at maximum level - worst_cp = _calc_cp(base_attack, base_defense, base_stamina, - 0, 0, 0, cp_m) - perfect_cp = _calc_cp(base_attack, base_defense, base_stamina, - cp_multiplier=cp_m) - current_cp = _calc_cp(base_attack, base_defense, base_stamina, - iv_attack, iv_defense, iv_stamina, cp_m) - cp_perfection = (current_cp - worst_cp) / (perfect_cp - worst_cp) - variants.append(cp_perfection) - - # get best value (probably for the best evolution) - cp_perfection = max(variants) - return cp_perfection - - def _get_moveset(self): - move1 = self.fast_attack - move2 = self.charged_attack - movesets = self._static_data['movesets'] - current_moveset = None - for moveset in movesets: # type: Moveset - if moveset.fast_attack == move1 and moveset.charged_attack == move2: - current_moveset = moveset - break - - if current_moveset is None: - raise Exception("Unexpected moveset [{}, {}] for #{} {}".format( - move1, move2, self.pokemon_id, self.name)) - - return current_moveset - - -class Attack(object): - def __init__(self, data): - # self._data = data # Not needed - all saved in fields - self.id = data['id'] - self.name = data['name'] - self.type = data['type'] - self.damage = data['damage'] - self.duration = data['duration'] / 1000.0 # duration in seconds - - # Energy addition for fast attack - # Energy cost for charged attack - self.energy = data['energy'] - - # Damage Per Second - # recalc for better precision - self.dps = self.damage / self.duration - - # Perfection of the attack in it's type (from 0 to 1) - self.rate_in_type = .0 - - @property - def damage_with_stab(self): - # damage with STAB (Same-type attack bonus) - return self.damage * STAB_FACTOR - - @property - def dps_with_stab(self): - # DPS with STAB (Same-type attack bonus) - return self.dps * STAB_FACTOR - - @property - def energy_per_second(self): - return self.energy / self.duration - - @property - def dodge_window(self): - # TODO: Attack Dodge Window - return NotImplemented - - @property - def is_charged(self): - return False - - def __str__(self): - return self.name - - def __repr__(self): - return self.name - - -class ChargedAttack(Attack): - def __init__(self, data): - super(ChargedAttack, self).__init__(data) - - @property - def is_charged(self): - return True - - -class Moveset(object): - def __init__(self, fm, chm, pokemon_types=(), pokemon_id=-1): - # type: (Attack, ChargedAttack, List[string], int) -> None - self.pokemon_id = pokemon_id - self.fast_attack = fm - self.charged_attack = chm - - # See Pokemons._process_movesets() - # See http://pokemongo.gamepress.gg/optimal-moveset-explanation - # See http://pokemongo.gamepress.gg/defensive-tactics - - fm_number = 100 # for simplicity we use 100 - - fm_energy = fm.energy * fm_number - fm_damage = fm.damage * fm_number - fm_secs = fm.duration * fm_number - - # Defender attacks in intervals of 1 second for the - # first 2 attacks, and then in intervals of 2 seconds - # So add 1.95 seconds to the quick move cool down for defense - # 1.95 is something like an average here - # TODO: Do something better? - fm_defense_secs = (fm.duration + 1.95) * fm_number - - chm_number = fm_energy / chm.energy - chm_damage = chm.damage * chm_number - chm_secs = chm.duration * chm_number - - damage_sum = fm_damage + chm_damage - # raw Damage-Per-Second for the moveset - self.dps = damage_sum / (fm_secs + chm_secs) - # average DPS for defense - self.dps_defense = damage_sum / (fm_defense_secs + chm_secs) - - # apply STAB (Same-type attack bonus) - if fm.type in pokemon_types: - fm_damage *= STAB_FACTOR - if chm.type in pokemon_types: - chm_damage *= STAB_FACTOR - - # DPS for attack (counting STAB) - self.dps_attack = (fm_damage + chm_damage) / (fm_secs + chm_secs) + return self._static_data['Next Evolution Requirements']['Amount'] - # Moveset perfection percent attack and for defense - # Calculated for current pokemon, not between all pokemons - # So 100% perfect moveset can be weak if pokemon is weak (e.g. Caterpie) - self.attack_perfection = .0 - self.defense_perfection = .0 + def _compute_iv(self): + total_IV = 0.0 + iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - # TODO: True DPS for real combat (floor(Attack/200 * MovePower * STAB) + 1) - # See http://pokemongo.gamepress.gg/pokemon-attack-explanation - - def __str__(self): - return '[{}, {}]'.format(self.fast_attack, self.charged_attack) - - def __repr__(self): - return '[{}, {}]'.format(self.fast_attack, self.charged_attack) + 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): @@ -752,47 +227,8 @@ def refresh(self): with open(user_web_inventory, 'w') as outfile: json.dump(inventory, outfile) -# -# Usage helpers - -# STAB (Same-type attack bonus) -STAB_FACTOR = 1.25 _inventory = None -LevelToCPm() # init LevelToCPm -FastAttacks() # init FastAttacks -ChargedAttacks() # init ChargedAttacks - - -def _calc_cp(base_attack, base_defense, base_stamina, - iv_attack=15, iv_defense=15, iv_stamina=15, - cp_multiplier=LevelToCPm.MAX_CPM): - """ - CP calculation - - CP = (Attack * Defense^0.5 * Stamina^0.5 * CP_Multiplier^2) / 10 - CP = (BaseAtk+AtkIV) * (BaseDef+DefIV)^0.5 * (BaseStam+StamIV)^0.5 * Lvl(CPScalar)^2 / 10 - - See https://www.reddit.com/r/TheSilphRoad/comments/4t7r4d/exact_pokemon_cp_formula/ - See https://www.reddit.com/r/pokemongodev/comments/4t7xb4/exact_cp_formula_from_stats_and_cpm_and_an_update/ - See http://pokemongo.gamepress.gg/pokemon-stats-advanced - See http://pokemongo.gamepress.gg/cp-multiplier - See http://gaming.stackexchange.com/questions/280491/formula-to-calculate-pokemon-go-cp-and-hp - - :param base_attack: Pokemon BaseAttack - :param base_defense: Pokemon BaseDefense - :param base_stamina: Pokemon BaseStamina - :param iv_attack: Pokemon IndividualAttack (0..15) - :param iv_defense: Pokemon IndividualDefense (0..15) - :param iv_stamina: Pokemon IndividualStamina (0..15) - :param cp_multiplier: CP Multiplier (0.79030001 is max - value for level 40) - :return: CP as float - """ - return (base_attack + iv_attack) \ - * ((base_defense + iv_defense)**0.5) \ - * ((base_stamina + iv_stamina)**0.5) \ - * (cp_multiplier ** 2) / 10 - def init_inventory(bot): global _inventory @@ -821,15 +257,3 @@ def pokemons(refresh=False): def items(): return _inventory.items - - -def levels_to_cpm(): - return LevelToCPm - - -def fast_attacks(): - return FastAttacks - - -def charged_attacks(): - return ChargedAttacks diff --git a/tests/inventory_test.py b/tests/inventory_test.py deleted file mode 100644 index 8362ce2b91..0000000000 --- a/tests/inventory_test.py +++ /dev/null @@ -1,183 +0,0 @@ -import unittest - -from pokemongo_bot.inventory import * - - -class InventoryTest(unittest.TestCase): - def test_pokemons(self): - # Init data - self.assertEqual(len(Pokemons().all()), 0) # No inventory loaded here - - obj = Pokemons - self.assertEqual(len(obj.STATIC_DATA), 151) - - for poke_info in obj.STATIC_DATA: - name = poke_info['Name'] - pokemon_id = int(poke_info['Number']) - self.assertTrue(1 <= pokemon_id <= 151) - - self.assertGreaterEqual(len(poke_info['movesets']), 1) - self.assertTrue(262 <= poke_info['max_cp'] <= 4145) - self.assertTrue(1 <= len(poke_info['types']) <= 2) - self.assertTrue(40 <= poke_info['BaseAttack'] <= 284) - self.assertTrue(20 <= poke_info['BaseDefense'] <= 500) - self.assertTrue(54 <= poke_info['BaseStamina'] <= 242) - self.assertTrue(.0 <= poke_info['CaptureRate'] <= .56) - self.assertTrue(.0 <= poke_info['FleeRate'] <= .99) - self.assertTrue(1 <= len(poke_info['Weaknesses']) <= 7) - self.assertTrue(3 <= len(name) <= 10) - - self.assertGreaterEqual(len(poke_info['Classification']), 11) - self.assertGreaterEqual(len(poke_info['Fast Attack(s)']), 1) - self.assertGreaterEqual(len(poke_info['Special Attack(s)']), 1) - - self.assertIs(obj.data_for(pokemon_id), poke_info) - self.assertIs(obj.name_for(pokemon_id), name) - - first_evolution_id = obj.first_evolution_id_for(pokemon_id) - self.assertGreaterEqual(first_evolution_id, 1) - next_evolution_ids = obj.next_evolution_ids_for(pokemon_id) - last_evolution_ids = obj.last_evolution_ids_for(pokemon_id) - candies_cost = obj.evolution_cost_for(pokemon_id) - obj.prev_evolution_id_for(pokemon_id) # just call test - self.assertGreaterEqual(len(last_evolution_ids), 1) - - if not obj.has_next_evolution(pokemon_id): - assert 'Next evolution(s)' not in poke_info - assert 'Next Evolution Requirements' not in poke_info - else: - self.assertGreaterEqual(len(next_evolution_ids), 1) - self.assertLessEqual(len(next_evolution_ids), len(last_evolution_ids)) - - reqs = poke_info['Next Evolution Requirements'] - self.assertEqual(reqs["Family"], first_evolution_id) - candies_name = obj.name_for(first_evolution_id) + ' candies' - self.assertEqual(reqs["Name"], candies_name) - self.assertIsNotNone(candies_cost) - self.assertTrue(12 <= candies_cost <= 400) - self.assertEqual(reqs["Amount"], candies_cost) - - evolutions = poke_info["Next evolution(s)"] - self.assertGreaterEqual(len(evolutions), len(next_evolution_ids)) - - for p in evolutions: - p_id = int(p["Number"]) - self.assertNotEqual(p_id, pokemon_id) - self.assertEqual(p["Name"], obj.name_for(p_id)) - - for p_id in next_evolution_ids: - self.assertEqual(obj.prev_evolution_id_for(p_id), pokemon_id) - prev_evs = obj.data_for(p_id)["Previous evolution(s)"] - self.assertGreaterEqual(len(prev_evs), 1) - self.assertEqual(int(prev_evs[-1]["Number"]), pokemon_id) - self.assertEqual(prev_evs[-1]["Name"], name) - - # Only Eevee has 3 next evolutions - self.assertEqual(len(next_evolution_ids), - 1 if pokemon_id != 133 else 3) - - if "Previous evolution(s)" in poke_info: - for p in poke_info["Previous evolution(s)"]: - p_id = int(p["Number"]) - self.assertNotEqual(p_id, pokemon_id) - self.assertEqual(p["Name"], obj.name_for(p_id)) - - # - # Specific pokemons testing - - poke = Pokemon({ - "num_upgrades": 2, "move_1": 210, "move_2": 69, "pokeball": 2, - "favorite": 1, "pokemon_id": 42, "battles_attacked": 4, - "stamina": 76, "stamina_max": 76, "individual_attack": 9, - "individual_defense": 4, "individual_stamina": 8, - "cp_multiplier": 0.4627983868122101, - "additional_cp_multiplier": 0.018886566162109375, - "cp": 653, "nickname": "Golb", "id": 13632861873471324}) - self.assertEqual(poke.level, 12.5) - self.assertEqual(poke.iv, 0.47) - self.assertAlmostEqual(poke.ivcp, 0.482845351) - self.assertAlmostEqual(poke.max_cp, 1921.34561459) - self.assertAlmostEqual(poke.cp_percent, 0.34000973) - self.assertTrue(poke.is_favorite) - self.assertEqual(poke.name, 'Golbat') - self.assertEqual(poke.nickname, "Golb") - self.assertAlmostEqual(poke.moveset.dps, 10.7540173053) - self.assertAlmostEqual(poke.moveset.dps_attack, 12.14462299) - self.assertAlmostEqual(poke.moveset.dps_defense, 4.876681614) - self.assertAlmostEqual(poke.moveset.attack_perfection, 0.4720730048) - self.assertAlmostEqual(poke.moveset.defense_perfection, 0.8158081497) - - poke = Pokemon({ - "move_1": 221, "move_2": 129, "pokemon_id": 19, "cp": 110, - "individual_attack": 6, "stamina_max": 22, "individual_defense": 14, - "cp_multiplier": 0.37523558735847473, "id": 7841053399}) - self.assertEqual(poke.level, 7.5) - self.assertEqual(poke.iv, 0.44) - self.assertAlmostEqual(poke.ivcp, 0.452398293) - self.assertAlmostEqual(poke.max_cp, 581.64643575) - self.assertAlmostEqual(poke.cp_percent, 0.189251848608) - self.assertFalse(poke.is_favorite) - self.assertEqual(poke.name, 'Rattata') - self.assertEqual(poke.nickname, 'Rattata') - self.assertAlmostEqual(poke.moveset.dps, 12.5567813108) - self.assertAlmostEqual(poke.moveset.dps_attack, 15.6959766385) - self.assertAlmostEqual(poke.moveset.dps_defense, 5.54282440561) - self.assertAlmostEqual(poke.moveset.attack_perfection, 0.835172881385) - self.assertAlmostEqual(poke.moveset.defense_perfection, 0.603137650999) - - def test_levels_to_cpm(self): - l2c = LevelToCPm - self.assertIs(levels_to_cpm(), l2c) - max_cpm = l2c.cp_multiplier_for(l2c.MAX_LEVEL) - self.assertEqual(l2c.MAX_LEVEL, 40) - self.assertEqual(l2c.MAX_CPM, max_cpm) - self.assertEqual(len(l2c.STATIC_DATA), 79) - - self.assertEqual(l2c.cp_multiplier_for("1"), 0.094) - self.assertEqual(l2c.cp_multiplier_for(1), 0.094) - self.assertEqual(l2c.cp_multiplier_for(1.0), 0.094) - self.assertEqual(l2c.cp_multiplier_for("17.5"), 0.558830576) - self.assertEqual(l2c.cp_multiplier_for(17.5), 0.558830576) - self.assertEqual(l2c.cp_multiplier_for('40.0'), 0.79030001) - self.assertEqual(l2c.cp_multiplier_for(40.0), 0.79030001) - self.assertEqual(l2c.cp_multiplier_for(40), 0.79030001) - - self.assertEqual(l2c.level_from_cpm(0.79030001), 40.0) - self.assertEqual(l2c.level_from_cpm(0.7903), 40.0) - - def test_attacks(self): - self._test_attacks(fast_attacks, FastAttacks) - self._test_attacks(charged_attacks, ChargedAttacks) - - def _test_attacks(self, callback, clazz): - charged = clazz is ChargedAttacks - self.assertIs(callback(), clazz) - - # check consistency - attacks = clazz.all_by_dps() - number = len(attacks) - self.assertTrue(number > 0) - self.assertGreaterEqual(len(clazz.BY_TYPE), 17) - self.assertEqual(number, len(clazz.all())) - self.assertEqual(number, len(clazz.STATIC_DATA)) - self.assertEqual(number, len(clazz.BY_NAME)) - self.assertEqual(number, sum([len(l) for l in clazz.BY_TYPE.values()])) - - # check data - prev_dps = float("inf") - for attack in attacks: # type: Attack - self.assertGreater(attack.id, 0) - self.assertGreater(len(attack.name), 0) - self.assertGreater(len(attack.type), 0) - self.assertGreaterEqual(attack.damage, 0) - self.assertGreater(attack.duration, .0) - self.assertGreater(attack.energy, 0) - self.assertGreaterEqual(attack.dps, 0) - self.assertTrue(.0 <= attack.rate_in_type <= 1.0) - self.assertLessEqual(attack.dps, prev_dps) - self.assertEqual(attack.is_charged, charged) - self.assertIs(attack, clazz.data_for(attack.id)) - self.assertIs(attack, clazz.by_name(attack.name)) - self.assertTrue(attack in clazz.BY_TYPE[attack.type]) - self.assertIsInstance(attack, ChargedAttack if charged else Attack) - prev_dps = attack.dps From 9ccadcd3af4da0cc0039d3552d26bba05e78ad99 Mon Sep 17 00:00:00 2001 From: sia84 Date: Thu, 11 Aug 2016 00:08:41 -0400 Subject: [PATCH 147/202] run.bat for windows (#3542) run.bat with persistent loop --- run.bat | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 run.bat diff --git a/run.bat b/run.bat new file mode 100644 index 0000000000..047c8f6fd4 --- /dev/null +++ b/run.bat @@ -0,0 +1,10 @@ +@echo off +set /a x=0 +:LOOP +echo Running pokecli.py for count: %x% +REM Change the path for python.exe if it's different for you +C:\Python27\python.exe pokecli.py +REM Waits for 60 seconds +ping 127.0.0.1 -n 60 > nul +set /a x+=1 +goto :LOOP From 847154d80805377a9c638871b3317b26ae6e0b4c Mon Sep 17 00:00:00 2001 From: Dmitry Ovodov Date: Thu, 11 Aug 2016 11:09:23 +0700 Subject: [PATCH 148/202] Fix error when MoveToFort called from handle_soft_ban.py (#3500) * Fix error when MoveToFort called from handle_soft_ban.py * Added myself to CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + pokemongo_bot/cell_workers/move_to_fort.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index a963396fcb..fc2478f20c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -61,3 +61,4 @@ * JaapMoolenaar * eevee-github * g0vanish + * cmezh diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 2be287e86c..24ecf5e74a 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -13,9 +13,13 @@ 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.ignore_item_count = self.config.get("ignore_item_count", False) + if self.config: + 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) + else: + self.lure_attraction = None + self.ignore_item_count = True def should_run(self): has_space_for_loot = self.bot.has_space_for_loot() From e2e32880deb32735b2811f1b10fe0997d1ec802a Mon Sep 17 00:00:00 2001 From: Amal Samally Date: Thu, 11 Aug 2016 08:35:25 +0400 Subject: [PATCH 149/202] Fixed list of charged attacks for Venonat (#3548) + BaseDefense/BaseStamina info fix (#3550) * Revert "Revert "Better inventory: attacks & movesets, IV CP perfection, pokemon level, etc.." (#3549)" This reverts commit e9b229ec0fd14a4814ea7431b1256850e907cfbf. * Fix BaseDefense/BaseStamina and type info Fixed BaseDefense/BaseStamina info (was mixed up) Fixed type info for Mr. Mime, Clefairy, Clefable, Jigglypuff, Wigglytuff Added check for correctness of exact CP value calculation * Fixed list of charged attacks for Venonat * Don't kill bot in case of unexpected moveset, use fallback + better error message --- .gitignore | 1 + data/charged_moves.json | 3 +- data/level_to_cpm.json | 81 +++++ data/pokemon.json | 632 +++++++++++++++++----------------- pokemongo_bot/inventory.py | 686 ++++++++++++++++++++++++++++++++++--- tests/inventory_test.py | 183 ++++++++++ 6 files changed, 1221 insertions(+), 365 deletions(-) create mode 100644 data/level_to_cpm.json create mode 100644 tests/inventory_test.py diff --git a/.gitignore b/.gitignore index 4721ce0253..06973c1249 100644 --- a/.gitignore +++ b/.gitignore @@ -100,6 +100,7 @@ share/ # PyCharm IDE settings .idea/ +*.iml # Personal load details src/ diff --git a/data/charged_moves.json b/data/charged_moves.json index 3d487b7201..c3f993191c 100644 --- a/data/charged_moves.json +++ b/data/charged_moves.json @@ -84,9 +84,8 @@ {"id":101,"name":"Flame Charge","type":"Fire","damage":25,"duration":3100,"energy":20,"dps":8.06}, {"id":34,"name":"Heart Stamp","type":"Psychic","damage":20,"duration":2550,"energy":25,"dps":7.84}, {"id":75,"name":"Parabolic Charge","type":"Electric","damage":15,"duration":2100,"energy":20,"dps":7.14}, -{"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":3700,"energy":20,"dps":6.75}, {"id":111,"name":"Icy Wind","type":"Ice","damage":25,"duration":3800,"energy":20,"dps":6.57}, {"id":84,"name":"Disarming Voice","type":"Fairy","damage":25,"duration":3900,"energy":20,"dps":6.41}, {"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":4000,"energy":20,"dps":6.25}, {"id":66,"name":"Shadow Sneak","type":"Ghost","damage":15,"duration":3100,"energy":20,"dps":4.83}, -{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] \ No newline at end of file +{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] diff --git a/data/level_to_cpm.json b/data/level_to_cpm.json new file mode 100644 index 0000000000..d2483d9a41 --- /dev/null +++ b/data/level_to_cpm.json @@ -0,0 +1,81 @@ +{ + "1": 0.094, + "1.5": 0.135137432, + "2": 0.16639787, + "2.5": 0.192650919, + "3": 0.21573247, + "3.5": 0.236572661, + "4": 0.25572005, + "4.5": 0.273530381, + "5": 0.29024988, + "5.5": 0.306057377, + "6": 0.3210876, + "6.5": 0.335445036, + "7": 0.34921268, + "7.5": 0.362457751, + "8": 0.37523559, + "8.5": 0.387592406, + "9": 0.39956728, + "9.5": 0.411193551, + "10": 0.42250001, + "10.5": 0.432926419, + "11": 0.44310755, + "11.5": 0.4530599578, + "12": 0.46279839, + "12.5": 0.472336083, + "13": 0.48168495, + "13.5": 0.4908558, + "14": 0.49985844, + "14.5": 0.508701765, + "15": 0.51739395, + "15.5": 0.525942511, + "16": 0.53435433, + "16.5": 0.542635767, + "17": 0.55079269, + "17.5": 0.558830576, + "18": 0.56675452, + "18.5": 0.574569153, + "19": 0.58227891, + "19.5": 0.589887917, + "20": 0.59740001, + "20.5": 0.604818814, + "21": 0.61215729, + "21.5": 0.619399365, + "22": 0.62656713, + "22.5": 0.633644533, + "23": 0.64065295, + "23.5": 0.647576426, + "24": 0.65443563, + "24.5": 0.661214806, + "25": 0.667934, + "25.5": 0.674577537, + "26": 0.68116492, + "26.5": 0.687680648, + "27": 0.69414365, + "27.5": 0.700538673, + "28": 0.70688421, + "28.5": 0.713164996, + "29": 0.71939909, + "29.5": 0.725571552, + "30": 0.7317, + "30.5": 0.734741009, + "31": 0.73776948, + "31.5": 0.740785574, + "32": 0.74378943, + "32.5": 0.746781211, + "33": 0.74976104, + "33.5": 0.752729087, + "34": 0.75568551, + "34.5": 0.758630378, + "35": 0.76156384, + "35.5": 0.764486065, + "36": 0.76739717, + "36.5": 0.770297266, + "37": 0.7731865, + "37.5": 0.776064962, + "38": 0.77893275, + "38.5": 0.781790055, + "39": 0.78463697, + "39.5": 0.787473578, + "40": 0.79030001 +} \ No newline at end of file diff --git a/data/pokemon.json b/data/pokemon.json index e64b7c31fb..44a22a9fd0 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -42,8 +42,8 @@ "Sludge Bomb" ], "BaseAttack": 126, - "BaseDefense": 90, - "BaseStamina": 126, + "BaseDefense": 126, + "BaseStamina": 90, "CaptureRate": 0.16, "FleeRate": 0.1 }, @@ -92,8 +92,8 @@ "Solar Beam" ], "BaseAttack": 156, - "BaseDefense": 120, - "BaseStamina": 158, + "BaseDefense": 158, + "BaseStamina": 120, "CaptureRate": 0.08, "FleeRate": 0.07 }, @@ -135,8 +135,8 @@ "Solar Beam" ], "BaseAttack": 198, - "BaseDefense": 160, - "BaseStamina": 200, + "BaseDefense": 200, + "BaseStamina": 160, "CaptureRate": 0.04, "FleeRate": 0.05 }, @@ -179,8 +179,8 @@ "Flamethrower" ], "BaseAttack": 128, - "BaseDefense": 78, - "BaseStamina": 108, + "BaseDefense": 108, + "BaseStamina": 78, "CaptureRate": 0.16, "FleeRate": 0.1 }, @@ -225,8 +225,8 @@ "Flamethrower" ], "BaseAttack": 160, - "BaseDefense": 116, - "BaseStamina": 140, + "BaseDefense": 140, + "BaseStamina": 116, "CaptureRate": 0.08, "FleeRate": 0.07 }, @@ -267,8 +267,8 @@ "Flamethrower" ], "BaseAttack": 212, - "BaseDefense": 156, - "BaseStamina": 182, + "BaseDefense": 182, + "BaseStamina": 156, "CaptureRate": 0.04, "FleeRate": 0.05 }, @@ -310,8 +310,8 @@ "Water Pulse" ], "BaseAttack": 112, - "BaseDefense": 88, - "BaseStamina": 142, + "BaseDefense": 142, + "BaseStamina": 88, "CaptureRate": 0.16, "FleeRate": 0.1 }, @@ -355,8 +355,8 @@ "Ice Beam" ], "BaseAttack": 144, - "BaseDefense": 118, - "BaseStamina": 176, + "BaseDefense": 176, + "BaseStamina": 118, "CaptureRate": 0.08, "FleeRate": 0.07 }, @@ -393,8 +393,8 @@ "Ice Beam" ], "BaseAttack": 186, - "BaseDefense": 158, - "BaseStamina": 222, + "BaseDefense": 222, + "BaseStamina": 158, "CaptureRate": 0.04, "FleeRate": 0.05 }, @@ -435,8 +435,8 @@ "Struggle" ], "BaseAttack": 62, - "BaseDefense": 90, - "BaseStamina": 66, + "BaseDefense": 66, + "BaseStamina": 90, "CaptureRate": 0.4, "FleeRate": 0.2 }, @@ -479,8 +479,8 @@ "Struggle" ], "BaseAttack": 56, - "BaseDefense": 100, - "BaseStamina": 86, + "BaseDefense": 86, + "BaseStamina": 100, "CaptureRate": 0.2, "FleeRate": 0.09 }, @@ -523,8 +523,8 @@ "Signal Beam" ], "BaseAttack": 144, - "BaseDefense": 120, - "BaseStamina": 144, + "BaseDefense": 144, + "BaseStamina": 120, "CaptureRate": 0.1, "FleeRate": 0.06 }, @@ -569,8 +569,8 @@ "Struggle" ], "BaseAttack": 68, - "BaseDefense": 80, - "BaseStamina": 64, + "BaseDefense": 64, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.2 }, @@ -617,8 +617,8 @@ "Struggle" ], "BaseAttack": 62, - "BaseDefense": 90, - "BaseStamina": 82, + "BaseDefense": 82, + "BaseStamina": 90, "CaptureRate": 0.2, "FleeRate": 0.09 }, @@ -706,8 +706,8 @@ } ], "BaseAttack": 94, - "BaseDefense": 80, - "BaseStamina": 90, + "BaseDefense": 90, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.2 }, @@ -754,8 +754,8 @@ } ], "BaseAttack": 126, - "BaseDefense": 126, - "BaseStamina": 122, + "BaseDefense": 122, + "BaseStamina": 126, "CaptureRate": 0.2, "FleeRate": 0.09 }, @@ -833,8 +833,8 @@ } ], "BaseAttack": 92, - "BaseDefense": 60, - "BaseStamina": 86, + "BaseDefense": 86, + "BaseStamina": 60, "CaptureRate": 0.4, "FleeRate": 0.2 }, @@ -866,8 +866,8 @@ } ], "BaseAttack": 146, - "BaseDefense": 110, - "BaseStamina": 150, + "BaseDefense": 150, + "BaseStamina": 110, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -908,8 +908,8 @@ "Twister" ], "BaseAttack": 102, - "BaseDefense": 80, - "BaseStamina": 78, + "BaseDefense": 78, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -945,8 +945,8 @@ "Twister" ], "BaseAttack": 168, - "BaseDefense": 130, - "BaseStamina": 146, + "BaseDefense": 146, + "BaseStamina": 130, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -984,8 +984,8 @@ "Wrap" ], "BaseAttack": 112, - "BaseDefense": 70, - "BaseStamina": 112, + "BaseDefense": 112, + "BaseStamina": 70, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -1018,8 +1018,8 @@ "Sludge Wave" ], "BaseAttack": 166, - "BaseDefense": 120, - "BaseStamina": 166, + "BaseDefense": 166, + "BaseStamina": 120, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -1056,8 +1056,8 @@ "Thunderbolt" ], "BaseAttack": 124, - "BaseDefense": 70, - "BaseStamina": 108, + "BaseDefense": 108, + "BaseStamina": 70, "CaptureRate": 0.16, "FleeRate": 0.1 }, @@ -1089,8 +1089,8 @@ "Thunder Punch" ], "BaseAttack": 200, - "BaseDefense": 120, - "BaseStamina": 154, + "BaseDefense": 154, + "BaseStamina": 120, "CaptureRate": 0.08, "FleeRate": 0.06 }, @@ -1129,8 +1129,8 @@ "Rock Tomb" ], "BaseAttack": 90, - "BaseDefense": 100, - "BaseStamina": 114, + "BaseDefense": 114, + "BaseStamina": 100, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -1164,8 +1164,8 @@ "Rock Tomb" ], "BaseAttack": 150, - "BaseDefense": 150, - "BaseStamina": 172, + "BaseDefense": 172, + "BaseStamina": 150, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -1207,8 +1207,8 @@ "Sludge Bomb" ], "BaseAttack": 100, - "BaseDefense": 110, - "BaseStamina": 104, + "BaseDefense": 104, + "BaseStamina": 110, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -1232,7 +1232,7 @@ "Previous evolution(s)": [ { "Number": "029", - "Name": "Nidoran F" + "Name": "Nidoran F" } ], "Next Evolution Requirements": { @@ -1252,8 +1252,8 @@ "Sludge Bomb" ], "BaseAttack": 132, - "BaseDefense": 140, - "BaseStamina": 136, + "BaseDefense": 136, + "BaseStamina": 140, "CaptureRate": 0.2, "FleeRate": 0.07 }, @@ -1295,8 +1295,8 @@ "Stone Edge" ], "BaseAttack": 184, - "BaseDefense": 180, - "BaseStamina": 190, + "BaseDefense": 190, + "BaseStamina": 180, "CaptureRate": 0.1, "FleeRate": 0.05 }, @@ -1338,8 +1338,8 @@ "Sludge Bomb" ], "BaseAttack": 110, - "BaseDefense": 92, - "BaseStamina": 94, + "BaseDefense": 94, + "BaseStamina": 92, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -1383,8 +1383,8 @@ "Sludge Bomb" ], "BaseAttack": 142, - "BaseDefense": 122, - "BaseStamina": 128, + "BaseDefense": 128, + "BaseStamina": 122, "CaptureRate": 0.2, "FleeRate": 0.07 }, @@ -1426,8 +1426,8 @@ "Sludge Wave" ], "BaseAttack": 204, - "BaseDefense": 162, - "BaseStamina": 170, + "BaseDefense": 170, + "BaseStamina": 162, "CaptureRate": 0.1, "FleeRate": 0.05 }, @@ -1436,7 +1436,7 @@ "Name": "Clefairy", "Classification": "Fairy Pokemon", "Type I": [ - "Normal" + "Fairy" ], "Weaknesses": [ "Fighting" @@ -1464,8 +1464,8 @@ "Moonblast" ], "BaseAttack": 116, - "BaseDefense": 140, - "BaseStamina": 124, + "BaseDefense": 124, + "BaseStamina": 140, "CaptureRate": 0.24, "FleeRate": 0.1 }, @@ -1474,7 +1474,7 @@ "Name": "Clefable", "Classification": "Fairy Pokemon", "Type I": [ - "Normal" + "Fairy" ], "Weaknesses": [ "Fighting" @@ -1497,8 +1497,8 @@ "Psychic" ], "BaseAttack": 178, - "BaseDefense": 190, - "BaseStamina": 178, + "BaseDefense": 178, + "BaseStamina": 190, "CaptureRate": 0.08, "FleeRate": 0.06 }, @@ -1537,8 +1537,8 @@ "Flamethrower" ], "BaseAttack": 106, - "BaseDefense": 76, - "BaseStamina": 118, + "BaseDefense": 118, + "BaseStamina": 76, "CaptureRate": 0.24, "FleeRate": 0.1 }, @@ -1572,8 +1572,8 @@ "Heat Wave" ], "BaseAttack": 176, - "BaseDefense": 146, - "BaseStamina": 194, + "BaseDefense": 194, + "BaseStamina": 146, "CaptureRate": 0.08, "FleeRate": 0.06 }, @@ -1584,6 +1584,9 @@ "Type I": [ "Normal" ], + "Type II": [ + "Fairy" + ], "Weaknesses": [ "Fighting" ], @@ -1610,8 +1613,8 @@ "Play Rough" ], "BaseAttack": 98, - "BaseDefense": 230, - "BaseStamina": 54, + "BaseDefense": 54, + "BaseStamina": 230, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -1622,6 +1625,9 @@ "Type I": [ "Normal" ], + "Type II": [ + "Fairy" + ], "Weaknesses": [ "Fighting" ], @@ -1643,8 +1649,8 @@ "Play Rough" ], "BaseAttack": 168, - "BaseDefense": 280, - "BaseStamina": 108, + "BaseDefense": 108, + "BaseStamina": 280, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -1687,8 +1693,8 @@ "Sludge Bomb" ], "BaseAttack": 88, - "BaseDefense": 80, - "BaseStamina": 90, + "BaseDefense": 90, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.2 }, @@ -1726,8 +1732,8 @@ "Poison Fang" ], "BaseAttack": 164, - "BaseDefense": 150, - "BaseStamina": 164, + "BaseDefense": 164, + "BaseStamina": 150, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -1774,8 +1780,8 @@ "Sludge Bomb" ], "BaseAttack": 134, - "BaseDefense": 90, - "BaseStamina": 130, + "BaseDefense": 130, + "BaseStamina": 90, "CaptureRate": 0.48, "FleeRate": 0.15 }, @@ -1824,8 +1830,8 @@ "Sludge Bomb" ], "BaseAttack": 162, - "BaseDefense": 120, - "BaseStamina": 158, + "BaseDefense": 158, + "BaseStamina": 120, "CaptureRate": 0.24, "FleeRate": 0.07 }, @@ -1867,8 +1873,8 @@ "Solar Beam" ], "BaseAttack": 202, - "BaseDefense": 150, - "BaseStamina": 190, + "BaseDefense": 190, + "BaseStamina": 150, "CaptureRate": 0.12, "FleeRate": 0.05 }, @@ -1913,8 +1919,8 @@ "X Scissor" ], "BaseAttack": 122, - "BaseDefense": 70, - "BaseStamina": 120, + "BaseDefense": 120, + "BaseStamina": 70, "CaptureRate": 0.32, "FleeRate": 0.15 }, @@ -1954,8 +1960,8 @@ "X Scissor" ], "BaseAttack": 162, - "BaseDefense": 120, - "BaseStamina": 170, + "BaseDefense": 170, + "BaseStamina": 120, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -1993,14 +1999,13 @@ } ], "Special Attack(s)": [ - "Dazzling Gleam", - "Psybeam", "Poison Fang", - "Shadow Ball" + "Psybeam", + "Signal Beam" ], "BaseAttack": 108, - "BaseDefense": 120, - "BaseStamina": 118, + "BaseDefense": 118, + "BaseStamina": 120, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -2038,8 +2043,8 @@ "Psychic" ], "BaseAttack": 172, - "BaseDefense": 140, - "BaseStamina": 154, + "BaseDefense": 154, + "BaseStamina": 140, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -2078,8 +2083,8 @@ "Rock Tomb" ], "BaseAttack": 108, - "BaseDefense": 20, - "BaseStamina": 86, + "BaseDefense": 86, + "BaseStamina": 20, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -2113,8 +2118,8 @@ "Stone Edge" ], "BaseAttack": 148, - "BaseDefense": 70, - "BaseStamina": 140, + "BaseDefense": 140, + "BaseStamina": 70, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -2151,8 +2156,8 @@ "Night Slash" ], "BaseAttack": 104, - "BaseDefense": 80, - "BaseStamina": 94, + "BaseDefense": 94, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -2184,8 +2189,8 @@ "Power Gem" ], "BaseAttack": 156, - "BaseDefense": 130, - "BaseStamina": 146, + "BaseDefense": 146, + "BaseStamina": 130, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -2223,8 +2228,8 @@ "Psybeam" ], "BaseAttack": 132, - "BaseDefense": 100, - "BaseStamina": 112, + "BaseDefense": 112, + "BaseStamina": 100, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -2257,8 +2262,8 @@ "Psychic" ], "BaseAttack": 194, - "BaseDefense": 160, - "BaseStamina": 176, + "BaseDefense": 176, + "BaseStamina": 160, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -2297,8 +2302,8 @@ "Low Sweep" ], "BaseAttack": 122, - "BaseDefense": 80, - "BaseStamina": 96, + "BaseDefense": 96, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -2332,8 +2337,8 @@ "Night Slash" ], "BaseAttack": 178, - "BaseDefense": 130, - "BaseStamina": 150, + "BaseDefense": 150, + "BaseStamina": 130, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -2450,8 +2455,8 @@ "Mud Bomb" ], "BaseAttack": 108, - "BaseDefense": 80, - "BaseStamina": 98, + "BaseDefense": 98, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -2495,8 +2500,8 @@ "Scald" ], "BaseAttack": 132, - "BaseDefense": 130, - "BaseStamina": 132, + "BaseDefense": 132, + "BaseStamina": 130, "CaptureRate": 0.2, "FleeRate": 0.07 }, @@ -2539,8 +2544,8 @@ "Submission" ], "BaseAttack": 180, - "BaseDefense": 180, - "BaseStamina": 202, + "BaseDefense": 202, + "BaseStamina": 180, "CaptureRate": 0.1, "FleeRate": 0.05 }, @@ -2582,8 +2587,8 @@ "Signal Beam" ], "BaseAttack": 110, - "BaseDefense": 50, - "BaseStamina": 76, + "BaseDefense": 76, + "BaseStamina": 50, "CaptureRate": 0.4, "FleeRate": 0.99 }, @@ -2628,8 +2633,8 @@ "Shadow Ball" ], "BaseAttack": 150, - "BaseDefense": 80, - "BaseStamina": 112, + "BaseDefense": 112, + "BaseStamina": 80, "CaptureRate": 0.2, "FleeRate": 0.07 }, @@ -2667,8 +2672,8 @@ "Shadow Ball" ], "BaseAttack": 186, - "BaseDefense": 110, - "BaseStamina": 152, + "BaseDefense": 152, + "BaseStamina": 110, "CaptureRate": 0.1, "FleeRate": 0.05 }, @@ -2711,8 +2716,8 @@ "Low Sweep" ], "BaseAttack": 118, - "BaseDefense": 140, - "BaseStamina": 96, + "BaseDefense": 96, + "BaseStamina": 140, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -2757,8 +2762,8 @@ "Submission" ], "BaseAttack": 154, - "BaseDefense": 160, - "BaseStamina": 144, + "BaseDefense": 144, + "BaseStamina": 160, "CaptureRate": 0.2, "FleeRate": 0.07 }, @@ -2844,8 +2849,8 @@ "Wrap" ], "BaseAttack": 158, - "BaseDefense": 100, - "BaseStamina": 78, + "BaseDefense": 78, + "BaseStamina": 100, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -2894,8 +2899,8 @@ "Sludge Bomb" ], "BaseAttack": 190, - "BaseDefense": 130, - "BaseStamina": 110, + "BaseDefense": 110, + "BaseStamina": 130, "CaptureRate": 0.2, "FleeRate": 0.07 }, @@ -2937,8 +2942,8 @@ "Solar Beam" ], "BaseAttack": 222, - "BaseDefense": 160, - "BaseStamina": 152, + "BaseDefense": 152, + "BaseStamina": 160, "CaptureRate": 0.1, "FleeRate": 0.05 }, @@ -2980,8 +2985,8 @@ "Wrap" ], "BaseAttack": 106, - "BaseDefense": 80, - "BaseStamina": 136, + "BaseDefense": 136, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -3018,8 +3023,8 @@ "Sludge Wave" ], "BaseAttack": 170, - "BaseDefense": 160, - "BaseStamina": 196, + "BaseDefense": 196, + "BaseStamina": 160, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -3068,8 +3073,8 @@ "Rock Tomb" ], "BaseAttack": 106, - "BaseDefense": 80, - "BaseStamina": 118, + "BaseDefense": 118, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -3120,8 +3125,8 @@ "Stone Edge" ], "BaseAttack": 142, - "BaseDefense": 110, - "BaseStamina": 156, + "BaseDefense": 156, + "BaseStamina": 110, "CaptureRate": 0.2, "FleeRate": 0.07 }, @@ -3165,8 +3170,8 @@ "Stone Edge" ], "BaseAttack": 176, - "BaseDefense": 160, - "BaseStamina": 198, + "BaseDefense": 198, + "BaseStamina": 160, "CaptureRate": 0.1, "FleeRate": 0.05 }, @@ -3205,8 +3210,8 @@ "Flame Wheel" ], "BaseAttack": 168, - "BaseDefense": 100, - "BaseStamina": 138, + "BaseDefense": 138, + "BaseStamina": 100, "CaptureRate": 0.32, "FleeRate": 0.1 }, @@ -3240,8 +3245,8 @@ "Heat Wave" ], "BaseAttack": 200, - "BaseDefense": 130, - "BaseStamina": 170, + "BaseDefense": 170, + "BaseStamina": 130, "CaptureRate": 0.12, "FleeRate": 0.06 }, @@ -3285,8 +3290,8 @@ "Water Pulse" ], "BaseAttack": 110, - "BaseDefense": 180, - "BaseStamina": 110, + "BaseDefense": 110, + "BaseStamina": 180, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -3325,8 +3330,8 @@ "Water Pulse" ], "BaseAttack": 184, - "BaseDefense": 190, - "BaseStamina": 198, + "BaseDefense": 198, + "BaseStamina": 190, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -3368,8 +3373,8 @@ "Thunderbolt" ], "BaseAttack": 128, - "BaseDefense": 50, - "BaseStamina": 138, + "BaseDefense": 138, + "BaseStamina": 50, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -3406,8 +3411,8 @@ "Magnet Bomb" ], "BaseAttack": 186, - "BaseDefense": 100, - "BaseStamina": 180, + "BaseDefense": 180, + "BaseStamina": 100, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -3437,8 +3442,8 @@ "Weight": "15.0 kg", "Height": "0.8 m", "BaseAttack": 138, - "BaseDefense": 104, - "BaseStamina": 132, + "BaseDefense": 132, + "BaseStamina": 104, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -3479,8 +3484,8 @@ "Swift" ], "BaseAttack": 126, - "BaseDefense": 70, - "BaseStamina": 96, + "BaseDefense": 96, + "BaseStamina": 70, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -3516,8 +3521,8 @@ "Drill Peck" ], "BaseAttack": 182, - "BaseDefense": 120, - "BaseStamina": 150, + "BaseDefense": 150, + "BaseStamina": 120, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -3555,8 +3560,8 @@ "Icy Wind" ], "BaseAttack": 104, - "BaseDefense": 130, - "BaseStamina": 138, + "BaseDefense": 138, + "BaseStamina": 130, "CaptureRate": 0.4, "FleeRate": 0.09 }, @@ -3594,8 +3599,8 @@ "Icy Wind" ], "BaseAttack": 156, - "BaseDefense": 180, - "BaseStamina": 192, + "BaseDefense": 192, + "BaseStamina": 180, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -3633,8 +3638,8 @@ "Sludge Bomb" ], "BaseAttack": 124, - "BaseDefense": 160, - "BaseStamina": 110, + "BaseDefense": 110, + "BaseStamina": 160, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -3667,8 +3672,8 @@ "Sludge Wave" ], "BaseAttack": 180, - "BaseDefense": 210, - "BaseStamina": 188, + "BaseDefense": 188, + "BaseStamina": 210, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -3706,8 +3711,8 @@ "Water Pulse" ], "BaseAttack": 120, - "BaseDefense": 60, - "BaseStamina": 112, + "BaseDefense": 112, + "BaseStamina": 60, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -3745,8 +3750,8 @@ "Icy Wind" ], "BaseAttack": 196, - "BaseDefense": 100, - "BaseStamina": 196, + "BaseDefense": 196, + "BaseStamina": 100, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -3793,8 +3798,8 @@ "Sludge Bomb" ], "BaseAttack": 136, - "BaseDefense": 60, - "BaseStamina": 82, + "BaseDefense": 82, + "BaseStamina": 60, "CaptureRate": 0.32, "FleeRate": 0.1 }, @@ -3843,8 +3848,8 @@ "Sludge Bomb" ], "BaseAttack": 172, - "BaseDefense": 90, - "BaseStamina": 118, + "BaseDefense": 118, + "BaseStamina": 90, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -3886,8 +3891,8 @@ "Sludge Wave" ], "BaseAttack": 204, - "BaseDefense": 120, - "BaseStamina": 156, + "BaseDefense": 156, + "BaseStamina": 120, "CaptureRate": 0.08, "FleeRate": 0.05 }, @@ -3921,8 +3926,8 @@ "Stone Edge" ], "BaseAttack": 90, - "BaseDefense": 70, - "BaseStamina": 186, + "BaseDefense": 186, + "BaseStamina": 70, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -3961,8 +3966,8 @@ "Psyshock" ], "BaseAttack": 104, - "BaseDefense": 120, - "BaseStamina": 140, + "BaseDefense": 140, + "BaseStamina": 120, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -3996,8 +4001,8 @@ "Shadow Ball" ], "BaseAttack": 162, - "BaseDefense": 170, - "BaseStamina": 196, + "BaseDefense": 196, + "BaseStamina": 170, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -4035,8 +4040,8 @@ "Water Pulse" ], "BaseAttack": 116, - "BaseDefense": 60, - "BaseStamina": 110, + "BaseDefense": 110, + "BaseStamina": 60, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -4069,8 +4074,8 @@ "X Scissor" ], "BaseAttack": 178, - "BaseDefense": 110, - "BaseStamina": 168, + "BaseDefense": 168, + "BaseStamina": 110, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -4107,8 +4112,8 @@ "Thunderbolt" ], "BaseAttack": 102, - "BaseDefense": 80, - "BaseStamina": 124, + "BaseDefense": 124, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -4140,8 +4145,8 @@ "Thunderbolt" ], "BaseAttack": 150, - "BaseDefense": 120, - "BaseStamina": 174, + "BaseDefense": 174, + "BaseStamina": 120, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -4186,8 +4191,8 @@ "Seed Bomb" ], "BaseAttack": 110, - "BaseDefense": 120, - "BaseStamina": 132, + "BaseDefense": 132, + "BaseStamina": 120, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -4228,8 +4233,8 @@ "Solar Beam" ], "BaseAttack": 232, - "BaseDefense": 190, - "BaseStamina": 164, + "BaseDefense": 164, + "BaseStamina": 190, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -4268,8 +4273,8 @@ "Dig" ], "BaseAttack": 102, - "BaseDefense": 100, - "BaseStamina": 150, + "BaseDefense": 150, + "BaseStamina": 100, "CaptureRate": 0.32, "FleeRate": 0.1 }, @@ -4303,8 +4308,8 @@ "Earthquake" ], "BaseAttack": 140, - "BaseDefense": 120, - "BaseStamina": 202, + "BaseDefense": 202, + "BaseStamina": 120, "CaptureRate": 0.12, "FleeRate": 0.06 }, @@ -4326,20 +4331,14 @@ ], "Weight": "49.8 kg", "Height": "1.5 m", - "Next evolution(s)": [ - { - "Number": "107", - "Name": "Hitmonchan" - } - ], "Special Attack(s)": [ "Low Sweep", "Stomp", "Stone Edge" ], "BaseAttack": 148, - "BaseDefense": 100, - "BaseStamina": 172, + "BaseDefense": 172, + "BaseStamina": 100, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -4361,12 +4360,6 @@ ], "Weight": "50.2 kg", "Height": "1.4 m", - "Previous evolution(s)": [ - { - "Number": "106", - "Name": "Hitmonlee" - } - ], "Special Attack(s)": [ "Brick Break", "Fire Punch", @@ -4374,8 +4367,8 @@ "Thunder Punch" ], "BaseAttack": 138, - "BaseDefense": 100, - "BaseStamina": 204, + "BaseDefense": 204, + "BaseStamina": 100, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -4401,8 +4394,8 @@ "Stomp" ], "BaseAttack": 126, - "BaseDefense": 180, - "BaseStamina": 160, + "BaseDefense": 160, + "BaseStamina": 180, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -4440,8 +4433,8 @@ "Sludge Bomb" ], "BaseAttack": 136, - "BaseDefense": 80, - "BaseStamina": 142, + "BaseDefense": 142, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -4474,8 +4467,8 @@ "Sludge Bomb" ], "BaseAttack": 190, - "BaseDefense": 130, - "BaseStamina": 198, + "BaseDefense": 198, + "BaseStamina": 130, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -4520,8 +4513,8 @@ "Stomp" ], "BaseAttack": 110, - "BaseDefense": 160, - "BaseStamina": 116, + "BaseDefense": 116, + "BaseStamina": 160, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -4561,8 +4554,8 @@ "Stone Edge" ], "BaseAttack": 166, - "BaseDefense": 210, - "BaseStamina": 160, + "BaseDefense": 160, + "BaseStamina": 210, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -4588,8 +4581,8 @@ "Psychic" ], "BaseAttack": 40, - "BaseDefense": 500, - "BaseStamina": 60, + "BaseDefense": 60, + "BaseStamina": 500, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -4618,8 +4611,8 @@ "Solar Beam" ], "BaseAttack": 164, - "BaseDefense": 130, - "BaseStamina": 152, + "BaseDefense": 152, + "BaseStamina": 130, "CaptureRate": 0.32, "FleeRate": 0.09 }, @@ -4645,8 +4638,8 @@ "Stomp" ], "BaseAttack": 142, - "BaseDefense": 210, - "BaseStamina": 178, + "BaseDefense": 178, + "BaseStamina": 210, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -4684,8 +4677,8 @@ "Flash Cannon" ], "BaseAttack": 122, - "BaseDefense": 60, - "BaseStamina": 100, + "BaseDefense": 100, + "BaseStamina": 60, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -4718,8 +4711,8 @@ "Hydro Pump" ], "BaseAttack": 176, - "BaseDefense": 110, - "BaseStamina": 150, + "BaseDefense": 150, + "BaseStamina": 110, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -4757,8 +4750,8 @@ "Water Pulse" ], "BaseAttack": 112, - "BaseDefense": 90, - "BaseStamina": 126, + "BaseDefense": 126, + "BaseStamina": 90, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -4830,8 +4823,8 @@ "Swift" ], "BaseAttack": 130, - "BaseDefense": 60, - "BaseStamina": 128, + "BaseDefense": 128, + "BaseStamina": 60, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -4870,8 +4863,8 @@ "Psybeam" ], "BaseAttack": 194, - "BaseDefense": 120, - "BaseStamina": 192, + "BaseDefense": 192, + "BaseStamina": 120, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -4882,6 +4875,9 @@ "Type I": [ "Psychic" ], + "Type II": [ + "Fairy" + ], "Weaknesses": [ "Bug", "Ghost", @@ -4899,8 +4895,8 @@ "Shadow Ball" ], "BaseAttack": 154, - "BaseDefense": 80, - "BaseStamina": 196, + "BaseDefense": 196, + "BaseStamina": 80, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -4933,8 +4929,8 @@ "X Scissor" ], "BaseAttack": 176, - "BaseDefense": 140, - "BaseStamina": 180, + "BaseDefense": 180, + "BaseStamina": 140, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -4968,8 +4964,8 @@ "Psyshock" ], "BaseAttack": 172, - "BaseDefense": 130, - "BaseStamina": 134, + "BaseDefense": 134, + "BaseStamina": 130, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -4995,8 +4991,8 @@ "Thunderbolt" ], "BaseAttack": 198, - "BaseDefense": 130, - "BaseStamina": 160, + "BaseDefense": 160, + "BaseStamina": 130, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -5024,8 +5020,8 @@ "Flamethrower" ], "BaseAttack": 214, - "BaseDefense": 130, - "BaseStamina": 158, + "BaseDefense": 158, + "BaseStamina": 130, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -5053,8 +5049,8 @@ "X Scissor" ], "BaseAttack": 184, - "BaseDefense": 130, - "BaseStamina": 186, + "BaseDefense": 186, + "BaseStamina": 130, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -5080,8 +5076,8 @@ "Iron Head" ], "BaseAttack": 148, - "BaseDefense": 150, - "BaseStamina": 184, + "BaseDefense": 184, + "BaseStamina": 150, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -5116,8 +5112,8 @@ "Struggle" ], "BaseAttack": 42, - "BaseDefense": 40, - "BaseStamina": 84, + "BaseDefense": 84, + "BaseStamina": 40, "CaptureRate": 0.56, "FleeRate": 0.15 }, @@ -5153,8 +5149,8 @@ "Twister" ], "BaseAttack": 192, - "BaseDefense": 190, - "BaseStamina": 196, + "BaseDefense": 196, + "BaseStamina": 190, "CaptureRate": 0.08, "FleeRate": 0.07 }, @@ -5186,8 +5182,8 @@ "Ice Beam" ], "BaseAttack": 186, - "BaseDefense": 260, - "BaseStamina": 190, + "BaseDefense": 190, + "BaseStamina": 260, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -5210,8 +5206,8 @@ "Weight": "4.0 kg", "Height": "0.3 m", "BaseAttack": 110, - "BaseDefense": 96, - "BaseStamina": 110, + "BaseDefense": 110, + "BaseStamina": 96, "CaptureRate": 0.16, "FleeRate": 0.1 }, @@ -5256,8 +5252,8 @@ "Swift" ], "BaseAttack": 114, - "BaseDefense": 110, - "BaseStamina": 128, + "BaseDefense": 128, + "BaseStamina": 110, "CaptureRate": 0.32, "FleeRate": 0.1 }, @@ -5289,8 +5285,8 @@ "Water Pulse" ], "BaseAttack": 186, - "BaseDefense": 260, - "BaseStamina": 168, + "BaseDefense": 168, + "BaseStamina": 260, "CaptureRate": 0.12, "FleeRate": 0.06 }, @@ -5321,8 +5317,8 @@ "Thunderbolt" ], "BaseAttack": 192, - "BaseDefense": 130, - "BaseStamina": 174, + "BaseDefense": 174, + "BaseStamina": 130, "CaptureRate": 0.12, "FleeRate": 0.06 }, @@ -5355,8 +5351,8 @@ "Heat Wave" ], "BaseAttack": 238, - "BaseDefense": 130, - "BaseStamina": 178, + "BaseDefense": 178, + "BaseStamina": 130, "CaptureRate": 0.12, "FleeRate": 0.06 }, @@ -5382,8 +5378,8 @@ "Signal Beam" ], "BaseAttack": 156, - "BaseDefense": 130, - "BaseStamina": 158, + "BaseDefense": 158, + "BaseStamina": 130, "CaptureRate": 0.32, "FleeRate": 0.09 }, @@ -5426,8 +5422,8 @@ "Rock Tomb" ], "BaseAttack": 132, - "BaseDefense": 70, - "BaseStamina": 160, + "BaseDefense": 160, + "BaseStamina": 70, "CaptureRate": 0.32, "FleeRate": 0.09 }, @@ -5465,8 +5461,8 @@ "Rock Slide" ], "BaseAttack": 180, - "BaseDefense": 140, - "BaseStamina": 202, + "BaseDefense": 202, + "BaseStamina": 140, "CaptureRate": 0.12, "FleeRate": 0.05 }, @@ -5509,8 +5505,8 @@ "Rock Tomb" ], "BaseAttack": 148, - "BaseDefense": 60, - "BaseStamina": 142, + "BaseDefense": 142, + "BaseStamina": 60, "CaptureRate": 0.32, "FleeRate": 0.09 }, @@ -5548,8 +5544,8 @@ "Water Pulse" ], "BaseAttack": 190, - "BaseDefense": 120, - "BaseStamina": 190, + "BaseDefense": 190, + "BaseStamina": 120, "CaptureRate": 0.12, "FleeRate": 0.05 }, @@ -5582,8 +5578,8 @@ "Iron Head" ], "BaseAttack": 182, - "BaseDefense": 160, - "BaseStamina": 162, + "BaseDefense": 162, + "BaseStamina": 160, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -5609,8 +5605,8 @@ "Hyper Beam" ], "BaseAttack": 180, - "BaseDefense": 320, - "BaseStamina": 180, + "BaseDefense": 180, + "BaseStamina": 320, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -5641,9 +5637,9 @@ "Weight": "55.4 kg", "Height": "1.7 m", "BaseAttack": 198, - "BaseDefense": 180, - "BaseStamina": 242, - "CaptureRate": 0.0, + "BaseDefense": 242, + "BaseStamina": 180, + "CaptureRate": 0, "FleeRate": 0.1 }, { @@ -5671,9 +5667,9 @@ "Weight": "52.6 kg", "Height": "1.6 m", "BaseAttack": 232, - "BaseDefense": 180, - "BaseStamina": 194, - "CaptureRate": 0.0, + "BaseDefense": 194, + "BaseStamina": 180, + "CaptureRate": 0, "FleeRate": 0.1 }, { @@ -5702,9 +5698,9 @@ "Weight": "60.0 kg", "Height": "2.0 m", "BaseAttack": 242, - "BaseDefense": 180, - "BaseStamina": 194, - "CaptureRate": 0.0, + "BaseDefense": 194, + "BaseStamina": 180, + "CaptureRate": 0, "FleeRate": 0.1 }, { @@ -5729,14 +5725,24 @@ "Family": 147, "Name": "Dratini candies" }, + "Next evolution(s)": [ + { + "Number": "148", + "Name": "Dragonair" + }, + { + "Number": "149", + "Name": "Dragonite" + } + ], "Special Attack(s)": [ "Aqua Tail", "Twister", "Wrap" ], "BaseAttack": 128, - "BaseDefense": 82, - "BaseStamina": 110, + "BaseDefense": 110, + "BaseStamina": 82, "CaptureRate": 0.32, "FleeRate": 0.09 }, @@ -5780,8 +5786,8 @@ "Wrap" ], "BaseAttack": 170, - "BaseDefense": 122, - "BaseStamina": 152, + "BaseDefense": 152, + "BaseStamina": 122, "CaptureRate": 0.08, "FleeRate": 0.06 }, @@ -5823,8 +5829,8 @@ "Hyper Beam" ], "BaseAttack": 250, - "BaseDefense": 182, - "BaseStamina": 212, + "BaseDefense": 212, + "BaseStamina": 182, "CaptureRate": 0.04, "FleeRate": 0.05 }, @@ -5852,9 +5858,9 @@ "Weight": "122.0 kg", "Height": "2.0 m", "BaseAttack": 284, - "BaseDefense": 212, - "BaseStamina": 202, - "CaptureRate": 0.0, + "BaseDefense": 202, + "BaseStamina": 212, + "CaptureRate": 0, "FleeRate": 0.1 }, { @@ -5886,9 +5892,9 @@ "Weight": "4.0 kg", "Height": "0.4 m", "BaseAttack": 220, - "BaseDefense": 200, - "BaseStamina": 220, - "CaptureRate": 0.0, + "BaseDefense": 220, + "BaseStamina": 200, + "CaptureRate": 0, "FleeRate": 0.1 } ] diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index ea81b7c093..d7f890933f 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1,26 +1,46 @@ import json +import logging import os + from pokemongo_bot.base_dir import _base_dir ''' 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 + +# +# Abstraction + +class _StaticInventoryComponent(object): STATIC_DATA_FILE = None # optionally load static data from file, # dropping the data in a static variable named STATIC_DATA + STATIC_DATA = None 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)) + cls.STATIC_DATA = cls.process_static_data( + json.load(open(cls.STATIC_DATA_FILE))) + + @classmethod + def process_static_data(cls, data): + # optional hook for processing the static data + # default is to use the data directly + return data + + +class _BaseInventoryComponent(_StaticInventoryComponent): + TYPE = None # base key name for items of this type + ID_FIELD = None # identifier field for items of this type + + def __init__(self): + self._data = {} + super(_BaseInventoryComponent, self).__init__() def parse(self, item): # optional hook for parsing the dict for this item @@ -42,34 +62,22 @@ def retrieve_data(self, inventory): def refresh(self, inventory): self._data = self.retrieve_data(inventory) - def get(self, id): - return self._data.get(id) + def get(self, object_id): + return self._data.get(object_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 +# +# Inventory Components class Candies(_BaseInventoryComponent): TYPE = 'candy' ID_FIELD = 'family_id' @classmethod - def family_id_for(self, pokemon_id): + def family_id_for(cls, pokemon_id): return Pokemons.first_evolution_id_for(pokemon_id) def get(self, pokemon_id): @@ -108,17 +116,120 @@ class Pokemons(_BaseInventoryComponent): ID_FIELD = 'id' STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'pokemon.json') - def parse(self, item): - if 'is_egg' in item: - return Egg(item) - return Pokemon(item) + @classmethod + def process_static_data(cls, data): + pokemon_id = 1 + for poke_info in data: + # prepare types + types = [poke_info['Type I'][0]] # required + for t in poke_info.get('Type II', []): + types.append(t) + poke_info['types'] = types + + # prepare attacks (moves) + cls._process_attacks(poke_info) + cls._process_attacks(poke_info, charged=True) + + # prepare movesets + poke_info['movesets'] = cls._process_movesets(poke_info, pokemon_id) + + # calculate maximum CP for the pokemon (best IVs, lvl 40) + base_attack = poke_info['BaseAttack'] + base_defense = poke_info['BaseDefense'] + base_stamina = poke_info['BaseStamina'] + max_cp = _calc_cp(base_attack, base_defense, base_stamina) + poke_info['max_cp'] = max_cp + + pokemon_id += 1 + return data + + @classmethod + def _process_movesets(cls, poke_info, pokemon_id): + # type: (dict, int) -> List[Moveset] + """ + The optimal moveset is the combination of two moves, one quick move + and one charge move, that deals the most damage over time. + + Because each quick move gains a certain amount of energy (different + for different moves) and each charge move requires a different amount + of energy to use, sometimes, a quick move with lower DPS will be + better since it charges the charge move faster. On the same note, + sometimes a charge move that has lower DPS will be more optimal since + it may require less energy or it may last for a longer period of time. + + Attacker have STAB (Same-type attack bonus - x1.25) pokemon have the + same type as attack. So we add it to the "Combo DPS" of the moveset. + + The defender attacks in intervals of 1 second for the first 2 attacks, + and then in intervals of 2 seconds for the remainder of the attacks. + This explains why we see two consecutive quick attacks at the beginning + of the match. As a result, we add +2 seconds to the DPS calculation + for defender DPS output. + + So to determine an optimal defensive moveset, we follow the same method + as we did for optimal offensive movesets, but instead calculate the + highest "Combo DPS" with an added 2 seconds to the quick move cool down. + + Note: critical hits have not yet been implemented in the game + + See http://pokemongo.gamepress.gg/optimal-moveset-explanation + See http://pokemongo.gamepress.gg/defensive-tactics + """ + + # Prepare movesets + movesets = [] + types = poke_info['types'] + for fm in poke_info['Fast Attack(s)']: + for chm in poke_info['Special Attack(s)']: + movesets.append(Moveset(fm, chm, types, pokemon_id)) + assert len(movesets) > 0 + + # Calculate attack perfection for each moveset + movesets = sorted(movesets, key=lambda m: m.dps_attack) + worst_dps = movesets[0].dps_attack + best_dps = movesets[-1].dps_attack + if best_dps > worst_dps: + for moveset in movesets: + current_dps = moveset.dps_attack + moveset.attack_perfection = \ + (current_dps - worst_dps) / (best_dps - worst_dps) + + # Calculate defense perfection for each moveset + movesets = sorted(movesets, key=lambda m: m.dps_defense) + worst_dps = movesets[0].dps_defense + best_dps = movesets[-1].dps_defense + if best_dps > worst_dps: + for moveset in movesets: + current_dps = moveset.dps_defense + moveset.defense_perfection = \ + (current_dps - worst_dps) / (best_dps - worst_dps) + + return sorted(movesets, key=lambda m: m.dps, reverse=True) + + @classmethod + def _process_attacks(cls, poke_info, charged=False): + # type: (dict, bool) -> List[Attack] + key = 'Fast Attack(s)' if not charged else 'Special Attack(s)' + moves_dict = (ChargedAttacks if charged else FastAttacks).BY_NAME + moves = [] + for name in poke_info[key]: + if name not in moves_dict: + raise KeyError('Unknown {} attack: "{}"'.format( + 'charged' if charged else 'fast', name)) + moves.append(moves_dict[name]) + moves = sorted(moves, key=lambda m: m.dps, reverse=True) + poke_info[key] = moves + assert len(moves) > 0 + return moves @classmethod def data_for(cls, pokemon_id): + # type: (int) -> dict return cls.STATIC_DATA[pokemon_id - 1] @classmethod def name_for(cls, pokemon_id): + # type: (int) -> string return cls.data_for(pokemon_id)['Name'] @classmethod @@ -129,24 +240,194 @@ def first_evolution_id_for(cls, pokemon_id): return pokemon_id @classmethod - def next_evolution_id_for(cls, pokemon_id): + def prev_evolution_id_for(cls, pokemon_id): + data = cls.data_for(pokemon_id) + if 'Previous evolution(s)' in data: + return int(data['Previous evolution(s)'][-1]['Number']) + return None + + @classmethod + def next_evolution_ids_for(cls, pokemon_id): try: - return int(cls.data_for(pokemon_id)['Next evolution(s)'][0]['Number']) + next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] except KeyError: - return None + return [] + # get only next level evolutions, not all possible + ids = [] + for p in next_evolutions: + p_id = int(p['Number']) + if cls.prev_evolution_id_for(p_id) == pokemon_id: + ids.append(p_id) + return ids @classmethod - def evolution_cost_for(cls, pokemon_id): + def last_evolution_ids_for(cls, pokemon_id): try: - return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] except KeyError: - return + return [pokemon_id] + # get only final evolutions, not all possible + ids = [] + for p in next_evolutions: + p_id = int(p['Number']) + if len(cls.data_for(p_id).get('Next evolution(s)', [])) == 0: + ids.append(p_id) + assert len(ids) > 0 + return ids + + @classmethod + def has_next_evolution(cls, pokemon_id): + poke_info = cls.data_for(pokemon_id) + return 'Next Evolution Requirements' in poke_info \ + or 'Next evolution(s)' in poke_info + + @classmethod + def evolution_cost_for(cls, pokemon_id): + if not cls.has_next_evolution(pokemon_id): + return None + return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + + def parse(self, item): + if 'is_egg' in item: + return Egg(item) + return Pokemon(item) 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)] + +# +# Static Components + +class LevelToCPm(_StaticInventoryComponent): + """ + Data for the CP multipliers at different levels + See http://pokemongo.gamepress.gg/cp-multiplier + See https://github.com/justinleewells/pogo-optimizer/blob/edd692d/data/game/level-to-cpm.json + """ + + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'level_to_cpm.json') + MAX_LEVEL = 40 + MAX_CPM = .0 + # half of the lowest difference between CPMs + HALF_DIFF_BETWEEN_HALF_LVL = 14e-3 + + @classmethod + def init_static_data(cls): + super(LevelToCPm, cls).init_static_data() + cls.MAX_CPM = cls.cp_multiplier_for(cls.MAX_LEVEL) + + @classmethod + def cp_multiplier_for(cls, level): + # type: (Union[float, int, string]) -> float + level = float(level) + level = str(int(level) if level.is_integer() else level) + return cls.STATIC_DATA[level] + + @classmethod + def level_from_cpm(cls, cp_multiplier): + # type: (float) -> float + for lvl, cpm in cls.STATIC_DATA.iteritems(): + diff = abs(cpm - cp_multiplier) + if diff <= cls.HALF_DIFF_BETWEEN_HALF_LVL: + return float(lvl) + raise ValueError("Unknown cp_multiplier: {}".format(cp_multiplier)) + + +class _Attacks(_StaticInventoryComponent): + BY_NAME = {} # type: Dict[string, Attack] + BY_TYPE = {} # type: Dict[List[Attack]] + BY_DPS = [] # type: List[Attack] + + @classmethod + def process_static_data(cls, moves): + ret = {} + by_type = {} + by_name = {} + fast = cls is FastAttacks + for attack in moves: + attack = Attack(attack) if fast else ChargedAttack(attack) + ret[attack.id] = attack + by_name[attack.name] = attack + + if attack.type not in by_type: + by_type[attack.type] = [] + by_type[attack.type].append(attack) + + for t in by_type.iterkeys(): + attacks = sorted(by_type[t], key=lambda m: m.dps, reverse=True) + min_dps = attacks[-1].dps + max_dps = attacks[0].dps - min_dps + if max_dps > .0: + for attack in attacks: # type: Attack + attack.rate_in_type = (attack.dps - min_dps) / max_dps + by_type[t] = attacks + + cls.BY_NAME = by_name + cls.BY_TYPE = by_type + cls.BY_DPS = sorted(ret.values(), key=lambda m: m.dps, reverse=True) + + return ret + + @classmethod + def data_for(cls, attack_id): + # type: (int) -> Attack + if attack_id not in cls.STATIC_DATA: + raise ValueError("Attack {} not found in {}".format( + attack_id, cls.__name__)) + return cls.STATIC_DATA[attack_id] + + @classmethod + def by_name(cls, name): + # type: (string) -> Attack + return cls.BY_NAME[name] + + @classmethod + def list_for_type(cls, type_name): + # type: (string) -> List[Attack] + """ + :return: Attacks sorted by DPS in descending order + """ + return cls.BY_TYPE[type_name] + + @classmethod + def all(cls): + return cls.STATIC_DATA.values() + + @classmethod + def all_by_dps(cls): + return cls.BY_DPS + + +class FastAttacks(_Attacks): + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'fast_moves.json') + + +class ChargedAttacks(_Attacks): + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'charged_moves.json') + + +# +# Instances + +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 Egg(object): def __init__(self, data): self._data = data @@ -158,52 +439,306 @@ def has_next_evolution(self): class Pokemon(object): def __init__(self, data): self._data = data + # Unique ID for this particular Pokemon self.id = data['id'] + # Id of the such pokemons in pokedex self.pokemon_id = data['pokemon_id'] + + # Combat points value self.cp = data['cp'] + # Base CP multiplier, fixed at the catch time + self.cp_bm = data['cp_multiplier'] + # Changeable part of the CP multiplier, increasing at power up + self.cp_am = data.get('additional_cp_multiplier', .0) + # Resulting CP multiplier + self.cp_m = self.cp_bm + self.cp_am + + # Current pokemon level (half of level is a normal value) + self.level = LevelToCPm.level_from_cpm(self.cp_m) + + # Maximum health points + self.hp_max = data['stamina_max'] + # Current health points + self.hp = data.get('stamina', self.hp_max) + assert 0 <= self.hp <= self.hp_max + + # Individial Values of the current pokemon (different for each pokemon) + self.iv_attack = data.get('individual_attack', 0) + self.iv_defense = data.get('individual_defense', 0) + self.iv_stamina = data.get('individual_stamina', 0) + self._static_data = Pokemons.data_for(self.pokemon_id) self.name = Pokemons.name_for(self.pokemon_id) - self.iv = self._compute_iv() + self.nickname = data.get('nickname', self.name) + self.in_fort = 'deployed_fort_id' in data self.is_favorite = data.get('favorite', 0) is 1 + # Basic Values of the current pokemon (identical for all such pokemons) + self.base_attack = self._static_data['BaseAttack'] + self.base_defense = self._static_data['BaseDefense'] + self.base_stamina = self._static_data['BaseStamina'] + + # Maximum possible CP for the current pokemon + self.max_cp = self._static_data['max_cp'] + + self.fast_attack = FastAttacks.data_for(data['move_1']) + self.charged_attack = ChargedAttacks.data_for(data['move_2']) # type: ChargedAttack + + # Internal values (IV) perfection percent + self.iv = self._compute_iv_perfection() + + # IV CP perfection - kind of IV perfection percent but calculated + # using weight of each IV in its contribution to CP of the best + # evolution of current pokemon + # So it tends to be more accurate than simple IV perfection + self.ivcp = self._compute_cp_perfection() + + # Exact value of current CP (not rounded) + self.cp_exact = _calc_cp( + self.base_attack, self.base_defense, self.base_stamina, + self.iv_attack, self.iv_defense, self.iv_stamina, self.cp_m) + assert max(int(self.cp_exact), 10) == self.cp + + # Percent of maximum possible CP + self.cp_percent = self.cp_exact / self.max_cp + + # Get moveset instance with calculated DPS and perfection percents + self.moveset = self._get_moveset() + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + def can_evolve_now(self): - return self.has_next_evolution() and self.candy_quantity >= self.evolution_cost + 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 + return Pokemons.has_next_evolution(self.pokemon_id) def has_seen_next_evolution(self): - return pokedex().captured(self.next_evolution_id) + for pokemon_id in self.next_evolution_ids: + if pokedex().captured(pokemon_id): + return True + return False @property - def next_evolution_id(self): - return Pokemons.next_evolution_id_for(self.pokemon_id) + def family_id(self): + return self.first_evolution_id @property def first_evolution_id(self): return Pokemons.first_evolution_id_for(self.pokemon_id) + @property + def prev_evolution_id(self): + return Pokemons.prev_evolution_id_for(self.pokemon_id) + + @property + def next_evolution_ids(self): + return Pokemons.next_evolution_ids_for(self.pokemon_id) + + @property + def last_evolution_ids(self): + return Pokemons.last_evolution_ids_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'] + return Pokemons.evolution_cost_for(self.pokemon_id) + + def _compute_iv_perfection(self): + total_iv = self.iv_attack + self.iv_defense + self.iv_stamina + iv_perfection = round((total_iv / 45.0), 2) + return iv_perfection + + def _compute_cp_perfection(self): + """ + CP perfect percent is more accurate than IV perfect + + We know attack plays an important role in CP, and different + pokemons have different base value, that's means 15/14/15 is + better than 14/15/15 for lot of pokemons, and if one pokemon's + base def is more than base sta, 15/15/14 is better than 15/14/15. + + See https://github.com/jabbink/PokemonGoBot/issues/469 + + So calculate CP perfection at final level for the best of the final + evolutions of the pokemon. + """ + variants = [] + iv_attack = self.iv_attack + iv_defense = self.iv_defense + iv_stamina = self.iv_stamina + cp_m = LevelToCPm.MAX_CPM + last_evolution_ids = self.last_evolution_ids + for pokemon_id in last_evolution_ids: + poke_info = Pokemons.data_for(pokemon_id) + base_attack = poke_info['BaseAttack'] + base_defense = poke_info['BaseDefense'] + base_stamina = poke_info['BaseStamina'] + + # calculate CP variants at maximum level + worst_cp = _calc_cp(base_attack, base_defense, base_stamina, + 0, 0, 0, cp_m) + perfect_cp = _calc_cp(base_attack, base_defense, base_stamina, + cp_multiplier=cp_m) + current_cp = _calc_cp(base_attack, base_defense, base_stamina, + iv_attack, iv_defense, iv_stamina, cp_m) + cp_perfection = (current_cp - worst_cp) / (perfect_cp - worst_cp) + variants.append(cp_perfection) + + # get best value (probably for the best evolution) + cp_perfection = max(variants) + return cp_perfection + + def _get_moveset(self): + move1 = self.fast_attack + move2 = self.charged_attack + movesets = self._static_data['movesets'] + current_moveset = None + for moveset in movesets: # type: Moveset + if moveset.fast_attack == move1 and moveset.charged_attack == move2: + current_moveset = moveset + break + + if current_moveset is None: + error = "Unexpected moveset [{}, {}] for #{} {}," \ + " please update info in pokemon.json and create issue/PR"\ + .format(move1, move2, self.pokemon_id, self.name) + # raise ValueError(error) + logging.getLogger(type(self).__name__).error(error) + current_moveset = Moveset( + move1, move2, self._static_data['types'], self.pokemon_id) + + return current_moveset + + +class Attack(object): + def __init__(self, data): + # self._data = data # Not needed - all saved in fields + self.id = data['id'] + self.name = data['name'] + self.type = data['type'] + self.damage = data['damage'] + self.duration = data['duration'] / 1000.0 # duration in seconds - def _compute_iv(self): - total_IV = 0.0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] + # Energy addition for fast attack + # Energy cost for charged attack + self.energy = data['energy'] - 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 + # Damage Per Second + # recalc for better precision + self.dps = self.damage / self.duration + + # Perfection of the attack in it's type (from 0 to 1) + self.rate_in_type = .0 + + @property + def damage_with_stab(self): + # damage with STAB (Same-type attack bonus) + return self.damage * STAB_FACTOR + + @property + def dps_with_stab(self): + # DPS with STAB (Same-type attack bonus) + return self.dps * STAB_FACTOR + + @property + def energy_per_second(self): + return self.energy / self.duration + + @property + def dodge_window(self): + # TODO: Attack Dodge Window + return NotImplemented + + @property + def is_charged(self): + return False + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + + +class ChargedAttack(Attack): + def __init__(self, data): + super(ChargedAttack, self).__init__(data) + + @property + def is_charged(self): + return True + + +class Moveset(object): + def __init__(self, fm, chm, pokemon_types=(), pokemon_id=-1): + # type: (Attack, ChargedAttack, List[string], int) -> None + if len(pokemon_types) <= 0 < pokemon_id: + pokemon_types = Pokemons.data_for(pokemon_id)['types'] + + self.pokemon_id = pokemon_id + self.fast_attack = fm + self.charged_attack = chm + + # See Pokemons._process_movesets() + # See http://pokemongo.gamepress.gg/optimal-moveset-explanation + # See http://pokemongo.gamepress.gg/defensive-tactics + + fm_number = 100 # for simplicity we use 100 + + fm_energy = fm.energy * fm_number + fm_damage = fm.damage * fm_number + fm_secs = fm.duration * fm_number + + # Defender attacks in intervals of 1 second for the + # first 2 attacks, and then in intervals of 2 seconds + # So add 1.95 seconds to the quick move cool down for defense + # 1.95 is something like an average here + # TODO: Do something better? + fm_defense_secs = (fm.duration + 1.95) * fm_number + + chm_number = fm_energy / chm.energy + chm_damage = chm.damage * chm_number + chm_secs = chm.duration * chm_number + + damage_sum = fm_damage + chm_damage + # raw Damage-Per-Second for the moveset + self.dps = damage_sum / (fm_secs + chm_secs) + # average DPS for defense + self.dps_defense = damage_sum / (fm_defense_secs + chm_secs) + + # apply STAB (Same-type attack bonus) + if fm.type in pokemon_types: + fm_damage *= STAB_FACTOR + if chm.type in pokemon_types: + chm_damage *= STAB_FACTOR + + # DPS for attack (counting STAB) + self.dps_attack = (fm_damage + chm_damage) / (fm_secs + chm_secs) + + # Moveset perfection percent attack and for defense + # Calculated for current pokemon, not between all pokemons + # So 100% perfect moveset can be weak if pokemon is weak (e.g. Caterpie) + self.attack_perfection = .0 + self.defense_perfection = .0 + + # TODO: True DPS for real combat (floor(Attack/200 * MovePower * STAB) + 1) + # See http://pokemongo.gamepress.gg/pokemon-attack-explanation + + def __str__(self): + return '[{}, {}]'.format(self.fast_attack, self.charged_attack) + + def __repr__(self): + return '[{}, {}]'.format(self.fast_attack, self.charged_attack) class Inventory(object): @@ -227,8 +762,47 @@ def refresh(self): with open(user_web_inventory, 'w') as outfile: json.dump(inventory, outfile) +# +# Usage helpers + +# STAB (Same-type attack bonus) +STAB_FACTOR = 1.25 _inventory = None +LevelToCPm() # init LevelToCPm +FastAttacks() # init FastAttacks +ChargedAttacks() # init ChargedAttacks + + +def _calc_cp(base_attack, base_defense, base_stamina, + iv_attack=15, iv_defense=15, iv_stamina=15, + cp_multiplier=LevelToCPm.MAX_CPM): + """ + CP calculation + + CP = (Attack * Defense^0.5 * Stamina^0.5 * CP_Multiplier^2) / 10 + CP = (BaseAtk+AtkIV) * (BaseDef+DefIV)^0.5 * (BaseStam+StamIV)^0.5 * Lvl(CPScalar)^2 / 10 + + See https://www.reddit.com/r/TheSilphRoad/comments/4t7r4d/exact_pokemon_cp_formula/ + See https://www.reddit.com/r/pokemongodev/comments/4t7xb4/exact_cp_formula_from_stats_and_cpm_and_an_update/ + See http://pokemongo.gamepress.gg/pokemon-stats-advanced + See http://pokemongo.gamepress.gg/cp-multiplier + See http://gaming.stackexchange.com/questions/280491/formula-to-calculate-pokemon-go-cp-and-hp + + :param base_attack: Pokemon BaseAttack + :param base_defense: Pokemon BaseDefense + :param base_stamina: Pokemon BaseStamina + :param iv_attack: Pokemon IndividualAttack (0..15) + :param iv_defense: Pokemon IndividualDefense (0..15) + :param iv_stamina: Pokemon IndividualStamina (0..15) + :param cp_multiplier: CP Multiplier (0.79030001 is max - value for level 40) + :return: CP as float + """ + return (base_attack + iv_attack) \ + * ((base_defense + iv_defense)**0.5) \ + * ((base_stamina + iv_stamina)**0.5) \ + * (cp_multiplier ** 2) / 10 + def init_inventory(bot): global _inventory @@ -257,3 +831,15 @@ def pokemons(refresh=False): def items(): return _inventory.items + + +def levels_to_cpm(): + return LevelToCPm + + +def fast_attacks(): + return FastAttacks + + +def charged_attacks(): + return ChargedAttacks diff --git a/tests/inventory_test.py b/tests/inventory_test.py new file mode 100644 index 0000000000..3d5ffd66b6 --- /dev/null +++ b/tests/inventory_test.py @@ -0,0 +1,183 @@ +import unittest + +from pokemongo_bot.inventory import * + + +class InventoryTest(unittest.TestCase): + def test_pokemons(self): + # Init data + self.assertEqual(len(Pokemons().all()), 0) # No inventory loaded here + + obj = Pokemons + self.assertEqual(len(obj.STATIC_DATA), 151) + + for poke_info in obj.STATIC_DATA: + name = poke_info['Name'] + pokemon_id = int(poke_info['Number']) + self.assertTrue(1 <= pokemon_id <= 151) + + self.assertGreaterEqual(len(poke_info['movesets']), 1) + self.assertTrue(262 <= poke_info['max_cp'] <= 4145) + self.assertTrue(1 <= len(poke_info['types']) <= 2) + self.assertTrue(40 <= poke_info['BaseAttack'] <= 284) + self.assertTrue(54 <= poke_info['BaseDefense'] <= 242) + self.assertTrue(20 <= poke_info['BaseStamina'] <= 500) + self.assertTrue(.0 <= poke_info['CaptureRate'] <= .56) + self.assertTrue(.0 <= poke_info['FleeRate'] <= .99) + self.assertTrue(1 <= len(poke_info['Weaknesses']) <= 7) + self.assertTrue(3 <= len(name) <= 10) + + self.assertGreaterEqual(len(poke_info['Classification']), 11) + self.assertGreaterEqual(len(poke_info['Fast Attack(s)']), 1) + self.assertGreaterEqual(len(poke_info['Special Attack(s)']), 1) + + self.assertIs(obj.data_for(pokemon_id), poke_info) + self.assertIs(obj.name_for(pokemon_id), name) + + first_evolution_id = obj.first_evolution_id_for(pokemon_id) + self.assertGreaterEqual(first_evolution_id, 1) + next_evolution_ids = obj.next_evolution_ids_for(pokemon_id) + last_evolution_ids = obj.last_evolution_ids_for(pokemon_id) + candies_cost = obj.evolution_cost_for(pokemon_id) + obj.prev_evolution_id_for(pokemon_id) # just call test + self.assertGreaterEqual(len(last_evolution_ids), 1) + + if not obj.has_next_evolution(pokemon_id): + assert 'Next evolution(s)' not in poke_info + assert 'Next Evolution Requirements' not in poke_info + else: + self.assertGreaterEqual(len(next_evolution_ids), 1) + self.assertLessEqual(len(next_evolution_ids), len(last_evolution_ids)) + + reqs = poke_info['Next Evolution Requirements'] + self.assertEqual(reqs["Family"], first_evolution_id) + candies_name = obj.name_for(first_evolution_id) + ' candies' + self.assertEqual(reqs["Name"], candies_name) + self.assertIsNotNone(candies_cost) + self.assertTrue(12 <= candies_cost <= 400) + self.assertEqual(reqs["Amount"], candies_cost) + + evolutions = poke_info["Next evolution(s)"] + self.assertGreaterEqual(len(evolutions), len(next_evolution_ids)) + + for p in evolutions: + p_id = int(p["Number"]) + self.assertNotEqual(p_id, pokemon_id) + self.assertEqual(p["Name"], obj.name_for(p_id)) + + for p_id in next_evolution_ids: + self.assertEqual(obj.prev_evolution_id_for(p_id), pokemon_id) + prev_evs = obj.data_for(p_id)["Previous evolution(s)"] + self.assertGreaterEqual(len(prev_evs), 1) + self.assertEqual(int(prev_evs[-1]["Number"]), pokemon_id) + self.assertEqual(prev_evs[-1]["Name"], name) + + # Only Eevee has 3 next evolutions + self.assertEqual(len(next_evolution_ids), + 1 if pokemon_id != 133 else 3) + + if "Previous evolution(s)" in poke_info: + for p in poke_info["Previous evolution(s)"]: + p_id = int(p["Number"]) + self.assertNotEqual(p_id, pokemon_id) + self.assertEqual(p["Name"], obj.name_for(p_id)) + + # + # Specific pokemons testing + + poke = Pokemon({ + "num_upgrades": 2, "move_1": 210, "move_2": 69, "pokeball": 2, + "favorite": 1, "pokemon_id": 42, "battles_attacked": 4, + "stamina": 76, "stamina_max": 76, "individual_attack": 9, + "individual_defense": 4, "individual_stamina": 8, + "cp_multiplier": 0.4627983868122101, + "additional_cp_multiplier": 0.018886566162109375, + "cp": 653, "nickname": "Golb", "id": 13632861873471324}) + self.assertEqual(poke.level, 12.5) + self.assertEqual(poke.iv, 0.47) + self.assertAlmostEqual(poke.ivcp, 0.488747515) + self.assertAlmostEqual(poke.max_cp, 1921.34561459) + self.assertAlmostEqual(poke.cp_percent, 0.340368964) + self.assertTrue(poke.is_favorite) + self.assertEqual(poke.name, 'Golbat') + self.assertEqual(poke.nickname, "Golb") + self.assertAlmostEqual(poke.moveset.dps, 10.7540173053) + self.assertAlmostEqual(poke.moveset.dps_attack, 12.14462299) + self.assertAlmostEqual(poke.moveset.dps_defense, 4.876681614) + self.assertAlmostEqual(poke.moveset.attack_perfection, 0.4720730048) + self.assertAlmostEqual(poke.moveset.defense_perfection, 0.8158081497) + + poke = Pokemon({ + "move_1": 221, "move_2": 129, "pokemon_id": 19, "cp": 106, + "individual_attack": 6, "stamina_max": 22, "individual_defense": 14, + "cp_multiplier": 0.37523558735847473, "id": 7841053399}) + self.assertEqual(poke.level, 7.5) + self.assertEqual(poke.iv, 0.44) + self.assertAlmostEqual(poke.ivcp, 0.3804059) + self.assertAlmostEqual(poke.max_cp, 581.64643575) + self.assertAlmostEqual(poke.cp_percent, 0.183759867) + self.assertFalse(poke.is_favorite) + self.assertEqual(poke.name, 'Rattata') + self.assertEqual(poke.nickname, 'Rattata') + self.assertAlmostEqual(poke.moveset.dps, 12.5567813108) + self.assertAlmostEqual(poke.moveset.dps_attack, 15.6959766385) + self.assertAlmostEqual(poke.moveset.dps_defense, 5.54282440561) + self.assertAlmostEqual(poke.moveset.attack_perfection, 0.835172881385) + self.assertAlmostEqual(poke.moveset.defense_perfection, 0.603137650999) + + def test_levels_to_cpm(self): + l2c = LevelToCPm + self.assertIs(levels_to_cpm(), l2c) + max_cpm = l2c.cp_multiplier_for(l2c.MAX_LEVEL) + self.assertEqual(l2c.MAX_LEVEL, 40) + self.assertEqual(l2c.MAX_CPM, max_cpm) + self.assertEqual(len(l2c.STATIC_DATA), 79) + + self.assertEqual(l2c.cp_multiplier_for("1"), 0.094) + self.assertEqual(l2c.cp_multiplier_for(1), 0.094) + self.assertEqual(l2c.cp_multiplier_for(1.0), 0.094) + self.assertEqual(l2c.cp_multiplier_for("17.5"), 0.558830576) + self.assertEqual(l2c.cp_multiplier_for(17.5), 0.558830576) + self.assertEqual(l2c.cp_multiplier_for('40.0'), 0.79030001) + self.assertEqual(l2c.cp_multiplier_for(40.0), 0.79030001) + self.assertEqual(l2c.cp_multiplier_for(40), 0.79030001) + + self.assertEqual(l2c.level_from_cpm(0.79030001), 40.0) + self.assertEqual(l2c.level_from_cpm(0.7903), 40.0) + + def test_attacks(self): + self._test_attacks(fast_attacks, FastAttacks) + self._test_attacks(charged_attacks, ChargedAttacks) + + def _test_attacks(self, callback, clazz): + charged = clazz is ChargedAttacks + self.assertIs(callback(), clazz) + + # check consistency + attacks = clazz.all_by_dps() + number = len(attacks) + self.assertTrue(number > 0) + self.assertGreaterEqual(len(clazz.BY_TYPE), 17) + self.assertEqual(number, len(clazz.all())) + self.assertEqual(number, len(clazz.STATIC_DATA)) + self.assertEqual(number, len(clazz.BY_NAME)) + self.assertEqual(number, sum([len(l) for l in clazz.BY_TYPE.values()])) + + # check data + prev_dps = float("inf") + for attack in attacks: # type: Attack + self.assertGreater(attack.id, 0) + self.assertGreater(len(attack.name), 0) + self.assertGreater(len(attack.type), 0) + self.assertGreaterEqual(attack.damage, 0) + self.assertGreater(attack.duration, .0) + self.assertGreater(attack.energy, 0) + self.assertGreaterEqual(attack.dps, 0) + self.assertTrue(.0 <= attack.rate_in_type <= 1.0) + self.assertLessEqual(attack.dps, prev_dps) + self.assertEqual(attack.is_charged, charged) + self.assertIs(attack, clazz.data_for(attack.id)) + self.assertIs(attack, clazz.by_name(attack.name)) + self.assertTrue(attack in clazz.BY_TYPE[attack.type]) + self.assertIsInstance(attack, ChargedAttack if charged else Attack) + prev_dps = attack.dps From f7975bb33cbc5e9a735782a0bc9dcf961a75b6ec Mon Sep 17 00:00:00 2001 From: Eli White Date: Wed, 10 Aug 2016 21:45:02 -0700 Subject: [PATCH 150/202] Blacklisting tejado from getting mentioned by the mention-bot --- .mention-bot | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .mention-bot diff --git a/.mention-bot b/.mention-bot new file mode 100644 index 0000000000..5635897ab2 --- /dev/null +++ b/.mention-bot @@ -0,0 +1,3 @@ +{ + "userBlacklist": ["tejado"] +} From a8242f7bda8cfb1b726173018a903fd4c58f87cc Mon Sep 17 00:00:00 2001 From: Eli White Date: Wed, 10 Aug 2016 22:00:09 -0700 Subject: [PATCH 151/202] Moving wiki pages to docs folder --- docs/auto_restart.md | 73 ++++++++++ docs/configuration_files.md | 271 ++++++++++++++++++++++++++++++++++++ docs/develop.md | 28 ++++ docs/docker.md | 25 ++++ docs/faq.md | 49 +++++++ docs/google_map.md | 54 +++++++ docs/installation.md | 93 +++++++++++++ docs/pokemon_iv.md | 58 ++++++++ 8 files changed, 651 insertions(+) create mode 100644 docs/auto_restart.md create mode 100644 docs/configuration_files.md create mode 100644 docs/develop.md create mode 100644 docs/docker.md create mode 100644 docs/faq.md create mode 100644 docs/google_map.md create mode 100644 docs/installation.md create mode 100644 docs/pokemon_iv.md diff --git a/docs/auto_restart.md b/docs/auto_restart.md new file mode 100644 index 0000000000..e85752018c --- /dev/null +++ b/docs/auto_restart.md @@ -0,0 +1,73 @@ +This page is for a workaround to restart your bot(s). +_(Restarting is superior over reconnecting in case of stability for crashes)_ + +# MAC OS +1. Open your terminal +Just open it and you finished step 1 + +2. Create a new apple script +Heres an example to start and restart bots (in separate folders) adjust it for your needs. (paths, start commands, restart timer, ...) + +You can create a start file (if you are lazy :P) and a restart file or just one for both needs + +Start script: + + tell application "Terminal" + activate + do script "cd desktop" in selected tab of the front window #edit your path + do script "cd bots" in selected tab of the front window #edit your path + do script "cd bot1" in selected tab of the front window #edit your path + do script "python pokecli.py" in selected tab of the front window #start with your parameters + + #add more bots + delay 10 + tell application "System Events" + keystroke "t" using {command down} #open a new tab for next bot + end tell + delay 5 + do script "cd .." in selected tab of the front window + do script "cd bot2" in selected tab of the front window + do script "python pokecli.py" in selected tab of the front window + #copy this part for the amount you need + end tell + +restart: + + repeat + + delay 1200 #timer in seconds + tell application "Terminal" + activate + + tell application "System Events" + keystroke "c" using {control down} #close the bot + end tell + delay 3 + do script "clear" in selected tab of the front window #not needed just for nice view + delay 3 + do script "python pokecli.py" in selected tab of the front window #restart with parameters + + #copy for the amount of bots + delay 10 + tell application "System Events" + keystroke "ö" using {command down} #going to the previous tab + end tell + delay 3 + + tell application "System Events" + keystroke "c" using {control down} + end tell + delay 3 + do script "clear" in selected tab of the front window + delay 3 + do script "python pokecli.py" in selected tab of the front window + + #copy for the amount of bots + tell application "System Events" + keystroke "ä" using {command down} #moving the the last tab + end tell + delay 3 + + end tell + + end repeat diff --git a/docs/configuration_files.md b/docs/configuration_files.md new file mode 100644 index 0000000000..a881740b9a --- /dev/null +++ b/docs/configuration_files.md @@ -0,0 +1,271 @@ +## Usage (up-to-date) + 1. copy `config.json.example` to `config.json`. + 2. Edit `config.json` and replace `auth_service`, `username`, `password`, `location` and `gmapkey` with your parameters (other keys are optional, check `Advance Configuration` below) + 3. Simply launch the script with : `./run.sh` or `./pokecli.py` or `python pokecli.py -cf ./configs/config.json` if you want to specify a config file + +## Advanced Configuration +| Parameter | Default | Description | +|------------------|-------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `tasks` | [] | The behaviors you want the bot to do. Read [how to configure tasks](#configuring-tasks). +| `max_steps` | 5 | The steps around your initial location (DEFAULT 5 mean 25 cells around your location) that will be explored +| `forts.avoid_circles` | False | Set whether the bot should avoid circles | +| `forts.max_circle_size` | 10 | How many forts to keep in ignore list | +| `walk` | 4.16 | Set the walking speed in kilometers per hour. (14 km/h is the maximum speed for egg hatching) | +| `action_wait_min` | 1 | Set the minimum time setting for anti-ban time randomizer +| `action_wait_max` | 4 | Set the maximum time setting for anti-ban time randomizer +| `debug` | false | Let the default value here except if you are developer | +| `test` | false | Let the default value here except if you are developer | | +| `location_cache` | true | Bot will start at last known location if you do not have location set in the config | +| `distance_unit` | km | Set the unit to display distance in (km for kilometers, mi for miles, ft for feet) | +| `evolve_cp_min` | 300 | Min. CP for evolve_all function + +## Configuring Tasks +The behaviors of the bot are configured via the `tasks` key in the `config.json`. This enables you to list what you want the bot to do and change the priority of those tasks by reordering them in the list. This list of tasks is run repeatedly and in order. For more information on why we are moving config to this format, check out the [original proposal](https://github.com/PokemonGoF/PokemonGo-Bot/issues/142). + +### Task Options: +* CatchLuredPokemon +* CatchVisiblePokemon +* EvolvePokemon + * `evolve_all`: Default `NONE` | Set to `"all"` to evolve Pokémon if possible when the bot starts. Can also be set to individual Pokémon as well as multiple separated by a comma. e.g "Pidgey,Rattata,Weedle,Zubat" + * `evolve_speed`: Default `20` + * `use_lucky_egg`: Default: `False` +* FollowPath + * `path_mode`: Default `loop` | Set the mode for the path navigator (loop or linear). + * `path_file`: Default `NONE` | Set the file containing the waypoints for the path navigator. +* FollowSpiral +* HandleSoftBan +* IncubateEggs + * `longer_eggs_first`: Default `True` +* MoveToFort +* [MoveToMapPokemon](#sniping-movetolocation) +* NicknamePokemon + * `nickname_template`: Default `""` | See the [Pokemon Nicknaming](#pokemon-nicknaming) section for more details +* RecycleItems + * `item_filter`: Pass a list of unwanted [items (using their JSON codes)](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Item-ID's) to recycle when collected at a Pokestop +* SpinFort +* TransferPokemon + +### Example configuration: +The following configuration tells the bot to transfer all the Pokemon that match the transfer configuration rules, then recycle the items that match its configuration, then catch the pokemon that it can, so on, so forth. Note the last two tasks, MoveToFort and FollowSpiral. When a task is still in progress, it won't run the next things in the list. So it will move towards the fort, on each step running through the list of tasks again. Only when it arrives at the fort and there are no other stops available for it to move towards will it continue to the next step and follow the spiral. + +``` +{ + // ... + "tasks": [ + { + "type": "TransferPokemon" + }, + { + "type": "RecycleItems" + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort" + }, + { + "type": "MoveToFort" + }, + { + "type": "FollowSpiral" + } + ] + // ... +} +``` + +### Specifying configuration for tasks +If you want to configure a given task, you can pass values like this: + +``` +{ + // ... + "tasks": [ + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + } + ] + // ... +} +``` + +### An example task configuration if you only wanted to collect items from forts: +``` +{ + // ... + "tasks": [ + { + "type": "RecycleItems" + }, + { + "type": "SpinFortWorker" + }, + { + "type": "MoveToFortWorker" + } + ], + // ... +} +``` + +## Catch Configuration +Default configuration will capture all Pokémon. + +```"any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}``` + +You can override the global configuration with Pokémon-specific options, such as: + +```"Pidgey": {"catch_above_cp": 0, "catch_above_iv": 0.8", "logic": "and"}``` to only capture Pidgey with a good roll. + +Additionally, you can specify always_capture and never_capture flags. + +For example: ```"Pidgey": {"never_capture": true}``` will stop catching Pidgey entirely. + +## Release Configuration + +### Common configuration + +Default configuration will not release any Pokémon. + +```"release": {"any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}}``` + +You can override the global configuration with Pokémon-specific options, such as: + +```"release": {"Pidgey": {"release_below_cp": 0, "release_below_iv": 0.8, "logic": "or"}}``` to only release Pidgey with bad rolls. + +Additionally, you can specify always_release and never_release flags. For example: + +```"release": {"Pidgey": {"always_release": true}}``` will release all Pidgey caught. + +### Keep the strongest pokemon configuration (dev branch) + +You can set ```"release": {"Pidgey": {"keep_best_cp": 1}}``` or ```"release": {"any": {"keep_best_iv": 1}}```. + +In that case after each capture bot will check that do you have a new Pokémon or not. + +If you don't have it, it will keep it (no matter was it strong or weak Pokémon). + +If you already have it, it will keep a stronger version and will transfer the a weaker one. + +```"release": {"any": {"keep_best_cp": 2}}```, ```"release": {"any": {"keep_best_cp": 10}}``` - can be any number. + +## Evolve All Configuration + +By setting the `evolve_all` attribute in config.json, you can instruct the bot to automatically +evolve specified Pokémon on startup. This is especially useful for batch-evolving after popping up +a lucky egg (currently this needs to be done manually). + +The evolve all mechanism evolves only higher IV/CP Pokémon. It works by sorting the high CP Pokémon (default: 300 CP or higher) +based on their IV values. After evolving all high CP Pokémon, the mechanism will move on to evolving lower CP Pokémon +only based on their CP (if it can). +It will also automatically transfer the evolved Pokémon based on the release configuration. + +Examples on how to use (set in config.json): + +1. "evolve_all": "all" + Will evolve ALL Pokémon. + +2. "evolve_all": "Pidgey,Weedle" + Will only evolve Pidgey and Weedle. + +3. Not setting evolve_all or having any other string would not evolve any Pokémon on startup. + +If you wish to change the default threshold of 300 CP, simply add the following to the config file: + +``` +"evolve_cp_min": +``` + +## Path Navigator Configuration + +Setting the `navigator.type` setting to `path` allows you to specify waypoints which the bot will follow. The waypoints can be loaded from a GPX or JSON file. By default the bot will walk along all specified waypoints and then move directly to the first waypoint again. When setting `navigator.path_mode` to `linear`, the bot will turn around at the last waypoint and along the given waypoints in reverse order. + +An example for a JSON file can be found in `configs/path.example.json`. GPX files can be exported from many online tools, such as gpsies.com.The bot loads the first segment of the first track. + +## Pokemon Nicknaming + +A `nickname_template` can be specified for the `NicknamePokemon` task to allow a nickname template to be applied to all pokemon in the user's inventory. For example, a user wanting all their pokemon to have their IV values as their nickname could use a template `{iv_ads}`, which will cause their pokemon to be named something like `13/7/12` (depending on the pokemon's actual IVs). + +The `NicknamePokemon` task will rename all pokemon in inventory on startup to match the given template and will rename any newly caught/hatched/evolved pokemon as the bot runs. _It may take one or two "ticks" after catching/hatching/evolving a pokemon for it to be renamed. This is intended behavior._ + +> **NOTE:** If you experience frequent `Pokemon not found` error messages, this is because the inventory cache has not been updated after a pokemon was released. This can be remedied by placing the `NicknamePokemon` task above the `TransferPokemon` task in your `config.json` file. + +Niantic imposes a 12-character limit on all pokemon nicknames, so any new nickname will be truncated to 12 characters if over that limit. Thus, it is up to the user to exercise judgment on what template will best suit their need with this constraint in mind. + +Because some pokemon have very long names, you can use the [Format String syntax](https://docs.python.org/2.7/library/string.html#formatstrings) to ensure that your names do not cause your templates to truncate. For example, using `{name:.8s}` causes the Pokemon name to never take up more than 8 characters in the nickname. This would help guarantee that a template like `{name:.8s}_{iv_pct}` never goes over the 12-character limit. + +Valid names in templates are: +- `name` = pokemon name +- `id` = pokemon type id (e.g. 1 for Bulbasaurs) +- `cp` = pokemon's CP +- `iv_attack` = pokemon's attack IV +- `iv_defense` = pokemon's defense IV +- `iv_stamina` = pokemon's stamina IV +- `iv_ads` = pokemon's IVs in `(attack)/(defense)/(stamina)` format (matches web UI format -- A/D/S) +- `iv_sum` = pokemon's IVs as a sum (e.g. 45 when 3 perfect 15 IVs) +- `iv_pct` = pokemon's IVs as a percentage (0-100) + +> **NOTE:** Use a blank template (`""`) to revert all pokemon to their original names (as if they had no nickname). + +Sample usages: +- `"{name}_{iv_pct}"` => `Mankey_69` +- `"{iv_pct}_{iv_ads}"` => `91_15/11/15` +- `""` -> `Mankey` +![sample](https://cloud.githubusercontent.com/assets/8896778/17285954/0fa44a88-577b-11e6-8204-b1302f4294bd.png) + +## Sniping _(MoveToLocation)_ +### Description +This task will fetch current pokemon spawns from /raw_data of an PokemonGo-Map instance. For information on how to properly setup PokemonGo-Map have a look at the Github page of the project [here](https://github.com/AHAAAAAAA/PokemonGo-Map/). There is an example config in `config/config.json.map.example` + +### Options +* `Address` - Address of the webserver of PokemonGo-Map. ex: `http://localhost:5000` +* `Mode` - Which mode to run snipin on + - `distance` - Will move to the nearest pokemon + - `priority` - Will move to the pokemon with the highest priority assigned (tie breaking by distance) +* `prioritize_vips` - Will prioritize vips in distance and priority mode above all normal pokemon if set to true +* `min_time` - Minimum time the pokemon has to be available before despawn +* `max_distance` - Maximum distance the pokemon is allowed to be when walking, ignored when sniping +* `snipe`: + - `True` - Will teleport to target pokemon, encounter it, teleport back then catch it + - `False` - Will walk normally to the pokemon +* `update_map` - disable/enable if the map location should be automatically updated to the bots current location +* `catch` - A dictionary of pokemon to catch with an assigned priority (higher => better) +* `snipe_high_prio_only` - Whether to snipe pokemon above a certain threshold. +* `snipe_high_prio_threshold` - The threshold number corresponding with the `catch` dictionary. Any pokemon above this threshold will be caught. Other will be igonored. + +#### Example +``` +{ + \\ ... + { + "type": "MoveToMapPokemon", + "config": { + "address": "http://localhost:5000", + "max_distance": 500, + "min_time": 60, + "min_ball": 50, + "prioritize_vips": true, + "snipe": true, + "snipe_high_prio_only": true, + "snipe_high_prio_threshold": 400, + "update_map": true, + "mode": "priority", + "catch": { + "Aerodactyl": 1000, + "Ditto": 900, + "Omastar": 500, + "Omanyte": 150, + "Caterpie": 10, + } + } + } + \\ ... +} +``` diff --git a/docs/develop.md b/docs/develop.md new file mode 100644 index 0000000000..7a1649219f --- /dev/null +++ b/docs/develop.md @@ -0,0 +1,28 @@ +> $ git clone --recursive -b dev https://github.com/PokemonGoF/PokemonGo-Bot +> $ cd PokemonGo-Bot +> // create virtualenv using Python 2.7 executable +> $ virtualenv -p C:\python27\python.exe venv +> $ source venv/Scripts/activate +> $ pip install -r requirements.txt + +Once you are you to date with [dev-branch] (https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) create a pull request and it will be re-viewed + + +### How to add/discover new API +The example is [here](https://github.com/PokemonGoF/PokemonGo-Bot/commit/46e2352ce9f349cc127a408959679282f9999585) +1. Check the type of your API request in [POGOProtos](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/RequestType.proto) For example: RECYCLE_INVENTORY_ITEM +2. Convert to the api call in pokemongo_bot/__init__.py, RECYCLE_INVENTORY_ITEM change to self.api.recycle_inventory_item +``` +def drop_item(self,item_id,count): + self.api.recycle_inventory_item(...............) +``` +3. Where is the param list? +You need check this [Requests/Messages/RecycleInventoryItemMessage.proto](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/Messages/RecycleInventoryItemMessage.proto) +4. Then our final api call is +``` +def drop_item(self,item_id,count): + self.api.recycle_inventory_item(item_id=item_id,count=count) + inventory_req = self.api.call() + print(inventory_req) +``` +5. You can now debug on the log to see if get what you need diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000000..04823a2f5c --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,25 @@ +Start by downloading for your platform: [Mac](https://www.docker.com/products/docker#/mac), [Windows](https://www.docker.com/products/docker#/windows), or [Linux](https://www.docker.com/products/docker#/linux). Once you have Docker installed, simply create the various config.json files for your different accounts (e.g. `configs/config-account1.json`) and then create a Docker image for PokemonGo-Bot using the Dockerfile in this repo. +``` +cd PokemonGo-Bot +docker build -t pokemongo-bot . +``` +You can verify that the image was created with: +``` +docker images +``` + +To run PokemonGo-Bot Docker image you've created, simple run: +``` +docker run --name=pokego-bot1 --rm -it -v $(pwd)/configs/config-account1.json:/usr/src/app/configs/config.json pokemongo-bot +``` +_Check the logs in real-time `docker logs -f pgobot`_ + +If you want to run multiple accounts with the same Docker image, simply specify different config.json and names in the Docker run command. +Do not push your image to a registry with your config.json and account details in it! + +Share web folder with host: +``` +docker run -it -v $(pwd)/web/:/usr/src/app/web --rm --name=pgo-bot-acct1 pokemongo-bot --config config.json +``` + +TODO: Add configuration for running multiple Docker containers from the same image for every bot instance, and a single container for the web UI. diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000000..7720f0a64c --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,49 @@ +### How do I start the application? +After customizing your config.json files, cd to the PokemonGo-Bot folder and enter: +``` +$ python pokecli.py +``` +This will start the application. + +### Python possible bug +If you encounter problems with the module `ssl` and it's function `_create_unverified_context`, just comment it. (Solution available in Python 2.7.11) +In order to comment out the function and the module, please follow the instructions below: +- edit `pokecli.py` +- put `#` before `if` (line 43) and `ssl` (line 44) +- save it + +Please keep in mind that this fix is only necessary if your python version don't have the `_create_unverified_context` argument in the ssl module. + +### What's IV? +Here's the [introduction](http://bulbapedia.bulbagarden.net/wiki/Individual_values) + +### Does it run automatically? +Not yet, still need a trainer to train the script param. But we are very close to. + +### Set GEO Location +It works, use "location": "59.333409,18.045008", in configs/config.json to set lat long for location. Use a Pokemon Go map to find an area with pokemons you still need (e.g. [https://pokevision.com/](https://pokevision.com/)), however don't jump too big distances (see "softban"). + +### Google login issues (Login Error, Server busy)? +Try to generate an [app password](!https://support.google.com/accounts/answer/185833?hl=en) and set is as +``` +-p "" +``` +This error mostly occurs for those who are using 2 factor authentication, but either way, for the purpose of security it would be nice to have a separate password for the bot app. + +### FLEE +The status code "3" corresponds to "Flee" - meaning your Pokemon has ran away. + {"responses": { "CATCH_POKEMON": { "status": 3 } } + +### My pokemon are not showing up in my Pokedex? +Finish the tutorial on a smartphone. This will then allow everything to be visible. + +### How can I maximise my XP per hour? +Quick Tip: When using this script, use a Lucky egg to double the XP for 30 mins. You will level up much faster. A Lucky egg is obtained on level 9 and further on whilst leveling up. (from VipsForever via /r/pokemongodev) + +### How do I use the map?? +[See wiki info here] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page)) + +### No JSON object could be decoded or decoder.py error +If you see "No JSON object could be decoded" or you see "decoder.py" in the last part of the error, this means that there is something wrong with your JSON. + +Copy the json in json files and copy it into http://jsonlint.com/ Then fix the error it gives you in your json. diff --git a/docs/google_map.md b/docs/google_map.md new file mode 100644 index 0000000000..7c6e3aca6c --- /dev/null +++ b/docs/google_map.md @@ -0,0 +1,54 @@ +The webpage is a submodule to this repository and config related to that is in ./web folder + +[OpenPoGoWeb] (https://github.com/OpenPoGo/OpenPoGoWeb) uses Google Maps. Read their [README] (https://github.com/OpenPoGo/OpenPoGoWeb/blob/master/README.md) for how to configure web frontend + +## How to set up a simple webserver with nginx +## SimpleHTTPServer +You can either view the map via opening the html file, or by serving it with SimpleHTTPServer (runs on localhost:8000) +To use SimpleHTTPServer: +``` +$ python -m SimpleHTTPServer [port] +``` +The default port is 8000, you can change that by giving a port number. Anything above port 1000 does not require root. +You will need to set your username(s) in the userdata.js file before opening, **Copy userdata.js.example to userdata.js** and edit with your favorite text editor. Put your username in the quotes instead of "username" +If using multiple usernames format like this +``` +var users = ["username1","username2"]; +``` +On Windows you can now go to http://127.0.0.1:8000 to see the map + + + +### Nginx on Ubuntu 14.x, 16.x +#### 1. Install nginx on your Ubuntu machine (e.g. on locally or AWS) +``` +sudo apt-get update +sudo apt-get install nginx +``` + +#### 2. Check the webserver +Check if the webserver is running by using your browser and entering the IP address of your local machine/server. +On a local machine this would be http://127.0.0.1. On AWS this is your public DNS if you haven't configured an elastic IP. + +#### 3. Change Base Directory of the Webserver +``` +sudo nano "/etc/nginx/sites-enabled/default" +``` +Comment out following line: ```root /var/www/html;``` and change it to the web folder of your PokemonGo-Bot: eg: +``` +/home/user/dev/PokemonGo-Bot/web; +``` +Use `nginx -s reload` to load the new configurations. + + +*** +Common Errors and Solutions + +> missing files: 127.0.0.1 - - "GET /catchable-YOURACCOUNT.json 404 +and location-SOMEACCOUNT.json 404 + +just create the file catachable-someaccount@gmail.com.json and put +``` +{} +``` +save and close repeat for other file. (location-SOMEACCOUNT.json) diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000000..8f5f5692e3 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,93 @@ +### Requirements (click each one for install guide) + +- [Python 2.7.x](http://docs.python-guide.org/en/latest/starting/installation/) +- [pip](https://pip.pypa.io/en/stable/installing/) +- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) +- [docker](https://docs.docker.com/engine/installation/) (Optional) - [how to setup after installation](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/How-to-run-with-Docker) +- [protobuf 3](https://github.com/google/protobuf) (OS Dependent, see below) + +### Protobuf 3 installation + +- OS X: `brew update && brew install --devel protobuf` +- Windows: Download protobuf 3.0: [here](https://github.com/google/protobuf/releases/download/v3.0.0-beta-4/protoc-3.0.0-beta-4-win32.zip) and unzip `bin/protoc.exe` into a folder in your PATH. +- Linux: `apt-get install python-protobuf` + +### Get encrypt.so (Windows part writing need fine tune) +We don't have the copyright of encrypt.so, please grab from internet and build your self.Take the risk as your own. +Example build sequence: +Create a new separate folder some here + +wget http://pgoapi.com/pgoencrypt.tar.gz && tar -xf pgoencrypt.tar.gz && cd pgoencrypt/src/ && make +Then copy libencrypt.so to the gofbot folder and rename to encrypt.so + +### Note on branch +Please keep in mind that master is not always up-to-date whereas 'dev' is. In the installation note below change `master` to `dev` if you want to get and use the latest version. + +## Update +To update your project do (in the project folder): `git pull` + +To update python requirement packages do (in the project folder): `pip install --upgrade -r requirements.txt` + +### Installation Linux +(change master to dev for the latest version) + +``` +$ git clone --recursive -b master https://github.com/PokemonGoF/PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ source bin/activate +$ pip install -r requirements.txt +``` +#### Example Installation for Ubuntu +(change dev to master for the lastest master version) + +http://pastebin.com/pzPjXT65 + + +### Installation Mac +(change master to dev for the latest version) + +``` +$ git clone --recursive -b master https://github.com/PokemonGoF/PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ source bin/activate +$ pip install -r requirements.txt +``` + +### Installation Windows +(change master to dev for the latest version) + +On Windows, you will need to install PyYaml through the installer and not through requirements.txt. + +##### Windows vista, 7, 8: +Go to : http://pyyaml.org/wiki/PyYAML , download the right version for your pc and install it + +##### Windows 10: +Go to [this](http://www.lfd.uci.edu/~gohlke/pythonlibs/#pyyaml) page and download: PyYAML-3.11-cp27-cp27m-win32.whl +(If running 64-bit python or if you get a 'not a supported wheel on this platform' error, +download the 64 bit version instead: PyYAML-3.11-cp27-cp27m-win_amd64.whl ) + +*(Run the following commands from Git Bash.)* + +``` +// switch to the directory where you downloaded PyYAML +$ cd download-directory +// install 32-bit version +$ pip2 install PyYAML-3.11-cp27-cp27m-win32.whl +// if you need to install the 64-bit version, do this instead: +// pip2 install PyYAML-3.11-cp27-cp27m-win_amd64.whl +``` + +After this, just do: + +``` +$ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ script\activate +$ pip2 install -r requirements.txt +$ git submodule init +$ git submodule update +``` diff --git a/docs/pokemon_iv.md b/docs/pokemon_iv.md new file mode 100644 index 0000000000..8662314fd5 --- /dev/null +++ b/docs/pokemon_iv.md @@ -0,0 +1,58 @@ +Individual Values, or IVs function like a Pokémon's "Genes". They are the traits which are passed down from one generation to the next. + +![](http://vignette3.wikia.nocookie.net/pokemon/images/d/dd/ImagesCAD6WL01.jpg/revision/latest?cb=20110511020243) + +**Individual values** + +Every stat has an IV ranging from 0 to 31 for each stat (HP, ATK, DEF, SPA, SPD, and SPE), and at level 100, their IVs are added to the Pokémon's stats for their total values. For example, a level 100 Tyranitar with no Effort Values and 0 IVs has 310 HP, however if it had 31 IVs, it would have 341 HP. + + +These stats are provided randomly for every Pokémon, caught or bred, and although as insignificant as 31 points may seem, they are required for Ace Trainers to obtain when breeding Pokémon with perfect natures/stats. On some occasions they are even the tipping point in a close matchup. For example, if there was a Terrakion with 0 Attack IV, it will have an attack of 358 at level 100 (with an attack improving nature), while a Terrakion with perfect Attack IVs would have 392 Attack. This small difference can mean the difference between a one-hit kill (not an OHKO) and survival with 1 HP. + + +**Breeding IVs** +Fortunately for trainers, Ace Trainers and Pokémon Breeders especially, IVs can be bred to obtain the perfect Pokémon. + +The process of breeding IVs is as follows, the example displayed below is to breed Nidorans: + +* The child's IV's are generated randomly, for example: 7/27/31/14/19/2, in HP/ATK/DEF/SPA/SPD/SPE format. +* Three stats are inherited from the parents, and are selected in three checks: +1. First check: A random stat (HP/ATK/DEF/SPA/SPD/SPE) is selected from either the Mother or the Father and passed on to the child. +2. Second check: A random stat with the exception of HP (ATK/DEF/SPA/SPD/SPE) is selected from either the Mother or the Father and passed on to the child. +3. Third check: A random stat with the exception of HP and DEF (ATK/SPA/SPD/SPE) is selected from either the Mother or the Father and passed on to the child. +This means that HP and DEF are less likely to pass on to the child, however there are ways to make sure the IVs are passed on. + +Letting either one of the parents hold a Power Item can ensure that the Power Item's respective stat will be passed on to the offspring from the parent that holds it. + +If the Power Item called Power Weight (doubles all HP EV gained) is held by a parent with a perfect IV of 31 for HP and the first check selects this parent, the child is ensured to have a perfect IV for HP. The other checks, though, will be random, and either luck or patience is required to eventually get the desired stats. + +Important: Only three stats are inherited per Pokémon, and these can stack. For example, the DEF IV can be inherited from both parents, thus rendering one redundant. + + + +**Checking IVs** +Beginning in Generation III, there has always been an NPC that allows players to check the IVs of their Pokémon. + + +If you wanted to check the IV's yourself the formula is as follows: + +The formula for HP is different from the rest of the stats, so here is the formula for HP: + +> IV=((Stat - Level Value - 10) * 100 / Level Value) - 2 * Base stat - (Math.Floor(EV/4)) + +In layman terms: + +> Individual Value= ((Current Stat Level - Current Level Value - 10) * 100 / Current Level Value) - 2 * Base Stat - (Math.Floor(EV/4)) + +Just in case you don't know (Math.Floor(EV/4)) means to take the amount of EVs you have in HP and divide it by 4 and then round down. +The formula you use for the rest of the stats is the same, so here it is: + +> IV=((Math.Ceiling(Stat/Nature) - 5) * 100 / Level Value) - 2 * Base Stat - (Math.Floor(EV/4)) + +In layman terms: + +> Individual Value= (Math.Ceiling(Current Stat Value/Nature Bonus) * 100 / Current Level Value) - 2 * Base Stat - (Math.Floor(EV/4)) + +Just in case you don't know (Math.Floor(EV/4)) means to take the amount of EVs you have in HP and divide it by 4 and then round down. + +Just in case you don't know (Math.Ceiling(Current Stat Value/Nature Bonus)) means to take the Current Stat Value and divide it by the bonus you get from the Pokémon's nature and then round up. If the stat gets an increase from the nature you divide the Current Stat Value by 1.1, and if it is a decrease from the nature you divide the Current Stat Value by 0.9. From d7cb7df27516b588252a767260176f92586489fa Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Wed, 10 Aug 2016 23:47:32 -0700 Subject: [PATCH 152/202] Dev merge to master, PR (#3564) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adding plugin support (#2679) * Adding plugin support * Adding an empty __init__.py * Moving the base task to the project root (#2702) * Moving the base task to the project root * Moving the base class more * Changing the import again * Adding a heartbeat to the analytics (#2709) * Adding a heartbeat to the analytics * Heartbeat every 30 seconds, not every 5 * Don't double track clients * Fix 'local variable 'bot' referenced before assignment' * Providing an error if tasks don't work for the given api (#2732) * Fix for utf8 encoding when catching lured pokemon (#2720) * Fixing lure pokestop encoding * fixing lure encoding * Fix For catchable not being displayed on the web (#2719) * Fix For catchable not being displayed on the web * Update catch_visible_pokemon.py * Added encrypt.so compilation process to Dockerfile (#2695) * OS Detection for encrypt lib (#2768) Fix 32bit check, darwin and linux use the same file Make it a function Check if file exists, if not show error Define file_name first Fix return Check if file exists, if not show error Print info about paths Fix for 32/64bit detection * Fix Typo in unexpected_response_retry (#2531) fixes #2525 #2523 * Revert "changing license from MIT to GPLv3" This reverts commit 69fb64f2bf7c12e28c2bb6d2b636c6af55822448. * When the google analytics domain is blocked the bot crashed. (#2764) With a simple try / except this can be solved. Fix dirty catch all * Fixes #2698 - Prevents "Possibly searching too often" error after re-login. (#2771) * Fixes #2698 - Added api.activate_signature call to prevent issue after re-login. - Also replaced deprecated log call with event_manager emit to prevent exception being thrown. * Modified to use OS detected library path as per PR #2768 * Support loading plugins from .zip files (#2766) * Keep track of how many pokemon released (#2884) * Setting Library path to work with encrypt.so (#2899) Setting LD_LIBRARY_PATH on Dockerfile * :sparkles: Added login and username to available stats (#2494) Added a player_data property in PokemonGoBot to access player data from outside Added unit tests for login and username stats Added tests for call args when updating the window title Added a platform-specific test for window title updating on win32 platform * [dev] small fixes (#2912) * Fixed emit_event typo * Update CONTRIBUTORS.md * Changed initialization location for "bot" We use bot in main exception on 128 * Update pokecli.py * Rename load_path to load_plugin (#2947) * Adding some logic for pulling plugins from github (#2967) * flush after title update (#2977) * correctly re-raise exception to keep backtrace (#2944) * Update MoveToMapPokemon to use events instead of logger. (#2913) * Config/encrypt.so (#2964) * Add config option for libencrypt.so * Correctly set the config value and check for the file in said dir * Fixed mispelling for "formatted" variable (#2984) * Loading plugins from Github (#2992) * Checking github plugin file existence * Loading plugins from github * Fixed #3000 (#3003) Fixed syntax error on "move_to_map_pokemon.py" that makes the client crash when using this feature. * Added MaxPotion inventory count to summary. (#3015) Short Description: The Max Potion count was missing from the inventory summary. Was #2456 * Added cleanup of download and files for encrypt.so after they are no longer needed (#3011) * Fix bot not returning back after telepoting (#3014) * Fix typo: last_long -> last_lon * Whitespace cleanup * Fix bug introduced by #3037: bot not returning back * Fix Dockerfile installation (#3057) * Fix for #3045 (#3055) * Added request to check configuration (#3089) * Fixed Dockerfile - missing \ on command lines (#3096) * Fixed mispelling for "formatted" variable * Docker commands missing trailing \ * Fix for FileIO slowing bot performance.This puts the map writing into a thread and makes sure it only executes once. (#3100) * Change word usage: "fled" to "escaped" (#3118) "fled" is confusing to lot of people and is easily confused with pokemon vanishing. "escaped" is a better term. * Update the example config file (#3120) * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Rename path.example.json to path.json.example * typo: logrmation -> information (#2601) Fix a typo. I assume that it was "information" initially, but became "logrmation" when someone used replace all functionality to replace all infos with logs. But I might be totally wrong at this point, idk. Just didn't like the word and wanted to fix that typo. * Change fled to escaped (#3129) Fix an issue after PR #3118 * When JSON parsing fails, give a rough indication of why (#3137) * When JSON parsing fails, give a rough indication of why * Use the official package instead of SHA1 commit * Handle Github Download Zip Format (#3108) * Checking github plugin file existence * Loading plugins from github * Starting install code for github plugins * Updating GithubPlugin to support extracting folders * Handling github zip formats by extracting to the correct location * Refactor catch worker (#2527) * refactor catch worker * fix * few renames * add to contributors * fix * add missing behavior * fix encounter events * don't make events about ignored pokemon * Added Run-Loop (#3143) * Add files via upload modified run script wich let you run the boot in a loop(if it crashes it restarts) * Integreated Loop into run.sh modified run.sh to loop the script so that even if it crashes it automaticly restarts. * fixing loop in spin fort task (#3165) * Some love for the vim users (#3154) * Updated README with link to desktop version (#3208) * Fix for #3190 (#3197) * MoveToMap: Add minimum balls to run (#3166) * added config to ignore item count for Spin and MoveToFort (#3160) * [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 * Keeping a cache of gym information (#3236) * New Option: "dont_nickname_favorite" (#2496) * New Option: "dont_nickname_favorite" This change (line 19) adds the option, that the user can choose, whether their favorite pokemons should also get a new nickname or not. If a user want this, then he or she has to add the line ("dont_nickname_favorite" = true) after ("nickname_template": " ... ",). * Update nickname_pokemon.py * Update * Put change to line 30 This reduce the reduce the runtime, because favorite pokemon won't be added to the list. * Restart the loop when catching pokemon and there are more to catch (#3242) * fixed NameError: global name 'pokemon_name' is not defined (#3244) resolves ```traceback (most recent call last): File "pokecli.py", line 521, in main() File "pokecli.py", line 95, in main bot.tick() File "/usr/src/app/pokemongo_bot/__init__.py", line 451, in tick if worker.work() == WorkerResult.RUNNING: File "/usr/src/app/pokemongo_bot/cell_workers/evolve_pokemon.py", line 38, in work self._execute_pokemon_evolve(pokemon, cache) File "/usr/src/app/pokemongo_bot/cell_workers/evolve_pokemon.py", line 117, in _execute_pokemon_evolve cache[pokemon.name] = 1 NameError: global name 'pokemon_name' is not defined``` * Stop fetching gym details (#3245) * Checking all forts for lured pokemon (#3163) * Fix flooding of keep_best_release (#3223) * Fix flooding of keep_best_release * Fix flooding of keep_best_release * [Feature] Recycle Threshold (#2465) * Add Threshold Option * Add Threshold Option to Example Configs * Add Name to Contributors * Change config name and message * Remove logger * Add option to run when storage less than something * Change Message * Fix * Error fixes, message improvement * Config Changes and Remove Option * Call heartbeat on step_walker even if speed is higher than distance (#2513) * Return an empty list if no pokemon are available. (#3259) The changes introduced in 4c95259 expose this bug. * Allow UpdateTitleStats to emit events instead of rewriting the console (#3264) * Updating our issue and PR templates to be more helpful (#3262) * Dev (#3277) * * adding enhanced sniping capabilities for move_to_map_pokemon * Adding enhanced sniping capabilities for move_to_map_pokemon * Update pgoapi to a newer version (#3241) This should hopefully fix issues like #3181, #3098, #2874 and potentially more. Needs testing/verification. I am running now, but it does take about an hour to trigger. * Fix unexpected egg incubation retry (#3276) incubator['used'] flag is set but not used in IncubateEggs._apply_incubators * has_next_evolution is a function not a property (#3284) * Powerful setup.sh (#3263) * Rewrite run.sh Very powerful run.sh with lots of function. 1.install(make .so) 2.update 3.config generator 4.config backup 5.run loop make it never down It should run like run.sh *.json or other opinion. See -help. * Update run.sh * Update run.sh OK problem solved * Delete setup.py * Rename run.sh to setup.sh * Create run.sh * Update setup.sh * Update install.sh * Update setup.sh * Update run.sh * Update setup.sh Some small fix. * Added +x to run.sh * Added a configuration option "path_startmode" (conflict merge #2489) (#3270) * Upstream update and merge, with path_startmode configuration * Removed logger and fixed base task path * As per request, path_startmode is now path_start_mode * Removed all logging * Adding documentation for how to use and write plugins (#3254) * Adding documentation for how to use and write plugins * Adding a link to the plugins docs in the Readme * Updating link to the plugin docs in the readme * Checking config file exists in run.sh (#3326) * Improve and update pokemon.json (#3331) 1. Unminify for simplier edits 2. Add BaseAttack, BaseDefense, BaseStamina, CaptureRate, FleeRate, Fast/Special Attack(s) * Made paths to .json files absolute so pokecli.py can be called from CRON (#3157) * Made paths to .json files absolute so pokecli.py can be called from CRON * made file paths abs in inventory fixed incorrect dict reference, changed to .get() as felt this was intended * Add fast & charged moves data from #2117 (originally by @iananass) (#3336) Data for pokemon quick & slow attacks * Upgrade pgoapi to the b4bf0e089dfe09903f8dda37dae56910e01f94cc commit(latest for now). (#3337) * Revert "Upgrade pgoapi to the b4bf0e089dfe09903f8dda37dae56910e01f94cc commit…" (#3340) * Added map_path configuration for move_to_map. (#3339) * Log stats on terminal (#3312) * added _log_on_terminal function * Added logic to toggle terminal logging functionality from config file * Added possibility to disable title changes, refactor code * Refactor tuples * Refactor ifs to clearer syntax * changes to improve event system based on new web ui devs requests * typo :D * let's use dict.get a bit to avoid errors * keeping the account in the remote command response * Add ColoredLoggingHandler (#3198) * Update TransferPokemon to use new Inventory Class (#3320) * Update TransferPokemon to use new Inventory Class * Use base_dir * Don't release pokemon if bot is on test mode * Some text fixes for setup.sh (#3390) Minor text fix. * Fix path of shells in install.sh (#3393) * Update install.sh Fix path * Update setup.sh fix path * Update run.sh fix path * Fix evolution error in pokemon.json (#3344) Fix evolution error in pokemon.json * Improve formatting consistency in transfer_pokemon.py (#3397) Improve formatting consistency * Remove unnecessary file * Put info on the next line in run.sh (#3422) * Update setup.sh fix typo * Update run.sh fix typo * Fix Struct() argument 1 must be string, not unicode. (#3375) * Give the possibility to disable a task without removing it (#3417) * Give the possiblity to disable a task in config without removing it from the config file * Put exmple only in nickname task * Add Unit testing * typo * Use enabled false as exemple * fix config creation (#3482) Changed auth to be more specifik and added right permissions. * Remove unused IV calculation from evolve_pokemon (#3487) Previously IV was computed in each worker. Now its fetched from inventory. This was left over and not called in the worker at all. * Don't show Inventory full event if we set "ignore_item_count" (#3440) * Fix showing the date in run.sh (#3433) fix the logic of showing the date * Typo fix: show new catch rate after berry throw. (#3521) * Fix stdout is not a terminal (#3511) * Ensure recycling happens if bag is over capacity. (#3531) Short Description: Ensures you that item Recycling happens if you have more items than the total bag capacity. When you level up, you are awarded items which can cause the bag to be over the capacity. * Better inventory: attacks & movesets, IV CP perfection, pokemon level, etc.. (#3455) * Add "level to CP multiplier" data Data is from justinleewells/pogo-optimizer: https://github.com/justinleewells/pogo-optimizer/blob/edd692d/data/game/level-to-cpm.json * Many improvements & additions for the inventory logic - LevelToCPm, FastAttacks, ChargedAttacks, Movesets - More info for each pokemon: attacks data, percent to max cp, IV CP perfection * Add PyCharm/IDEA *.iml (project file) to ignored * Fixes, improvements & refactoring for inventory.py - Return inadvertently deleted pieces of code (thanks to @achretien) - Evolution logic fixes - Other minor fixes - Moveset logic moved to Moveset class * Fix data for pokemons & charged moves * Inventory tests: pokemon data, LevelToCPm, attacks * Fix travis build * Fix info for Hitmonlee & Hitmonchan * Revert "Better inventory: attacks & movesets, IV CP perfection, pokemon level, etc.." (#3549) * run.bat for windows (#3542) run.bat with persistent loop * Fix error when MoveToFort called from handle_soft_ban.py (#3500) * Fix error when MoveToFort called from handle_soft_ban.py * Added myself to CONTRIBUTORS.md * Fixed list of charged attacks for Venonat (#3548) + BaseDefense/BaseStamina info fix (#3550) * Revert "Revert "Better inventory: attacks & movesets, IV CP perfection, pokemon level, etc.." (#3549)" This reverts commit e9b229ec0fd14a4814ea7431b1256850e907cfbf. * Fix BaseDefense/BaseStamina and type info Fixed BaseDefense/BaseStamina info (was mixed up) Fixed type info for Mr. Mime, Clefairy, Clefable, Jigglypuff, Wigglytuff Added check for correctness of exact CP value calculation * Fixed list of charged attacks for Venonat * Don't kill bot in case of unexpected moveset, use fallback + better error message * Blacklisting tejado from getting mentioned by the mention-bot * Moving wiki pages to docs folder --- .gitignore | 1 + .mention-bot | 3 + CONTRIBUTORS.md | 3 + configs/config.json.cluster.example | 1 + configs/config.json.example | 8 + configs/config.json.map.example | 2 + configs/config.json.path.example | 1 + configs/config.json.pokemon.example | 1 + data/charged_moves.json | 91 + data/fast_moves.json | 41 + data/level_to_cpm.json | 81 + data/pokemon.json | 5901 ++++++++++++++++- docs/auto_restart.md | 73 + docs/configuration_files.md | 271 + docs/develop.md | 28 + docs/docker.md | 25 + docs/faq.md | 49 + docs/google_map.md | 54 + docs/installation.md | 93 + docs/pokemon_iv.md | 58 + install.sh | 10 +- pokecli.py | 11 +- pokemongo_bot/__init__.py | 68 +- pokemongo_bot/base_dir.py | 4 + pokemongo_bot/base_task.py | 1 + .../cell_workers/catch_visible_pokemon.py | 4 +- pokemongo_bot/cell_workers/evolve_pokemon.py | 5 - pokemongo_bot/cell_workers/move_to_fort.py | 12 +- .../cell_workers/move_to_map_pokemon.py | 8 +- .../cell_workers/pokemon_catch_worker.py | 20 +- pokemongo_bot/cell_workers/recycle_items.py | 5 +- pokemongo_bot/cell_workers/spin_fort.py | 11 +- .../cell_workers/transfer_pokemon.py | 204 +- .../cell_workers/update_title_stats.py | 53 +- pokemongo_bot/event_handlers/__init__.py | 1 + .../event_handlers/colored_logging_handler.py | 172 + pokemongo_bot/inventory.py | 704 +- pokemongo_bot/socketio_server/app.py | 8 +- pokemongo_bot/step_walker.py | 11 + pokemongo_bot/tree_config_builder.py | 3 +- pokemongo_bot/websocket_remote_control.py | 9 +- run.bat | 10 + run.sh | 13 +- setup.sh | 23 +- tests/inventory_test.py | 183 + tests/tree_config_builder_test.py | 19 + 46 files changed, 8087 insertions(+), 270 deletions(-) create mode 100644 .mention-bot create mode 100644 data/charged_moves.json create mode 100644 data/fast_moves.json create mode 100644 data/level_to_cpm.json create mode 100644 docs/auto_restart.md create mode 100644 docs/configuration_files.md create mode 100644 docs/develop.md create mode 100644 docs/docker.md create mode 100644 docs/faq.md create mode 100644 docs/google_map.md create mode 100644 docs/installation.md create mode 100644 docs/pokemon_iv.md create mode 100644 pokemongo_bot/base_dir.py create mode 100644 pokemongo_bot/event_handlers/colored_logging_handler.py create mode 100644 run.bat create mode 100644 tests/inventory_test.py diff --git a/.gitignore b/.gitignore index 4721ce0253..06973c1249 100644 --- a/.gitignore +++ b/.gitignore @@ -100,6 +100,7 @@ share/ # PyCharm IDE settings .idea/ +*.iml # Personal load details src/ diff --git a/.mention-bot b/.mention-bot new file mode 100644 index 0000000000..5635897ab2 --- /dev/null +++ b/.mention-bot @@ -0,0 +1,3 @@ +{ + "userBlacklist": ["tejado"] +} diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 57b289ab90..fc2478f20c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -59,3 +59,6 @@ * nikhil-pandey * thebigjc * JaapMoolenaar + * eevee-github + * g0vanish + * cmezh diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index e4597519f6..eb507cd43c 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -81,6 +81,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.example b/configs/config.json.example index d2e8d4f064..59974ba156 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -21,6 +21,13 @@ { "type": "TransferPokemon" }, + { + "type": "NicknamePokemon", + "config": { + "enabled": false, + "nickname_template": "{iv_pct}_{iv_ads}" + } + }, { "type": "EvolvePokemon", "config": { @@ -89,6 +96,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.map.example b/configs/config.json.map.example index bb6878f5ac..cf6604976b 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -66,6 +66,7 @@ "snipe_high_prio_threshold": 400, "update_map": true, "mode": "priority", + "map_path": "raw_data", "catch": { "==========Legendaries==========": 0, "Aerodactyl": 1000, @@ -322,6 +323,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 6f7b04c305..6b6619573b 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -83,6 +83,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "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 e7ba38dc37..1dfa01199d 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -89,6 +89,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or" }, diff --git a/data/charged_moves.json b/data/charged_moves.json new file mode 100644 index 0000000000..c3f993191c --- /dev/null +++ b/data/charged_moves.json @@ -0,0 +1,91 @@ +[{"id":32,"name":"Stone Edge","type":"Rock","damage":80,"duration":3100,"energy":100,"dps":25.8}, +{"id":28,"name":"Cross Chop","type":"Fighting","damage":60,"duration":2000,"energy":100,"dps":30.0}, +{"id":83,"name":"Dragon Claw","type":"Dragon","damage":35,"duration":1500,"energy":50,"dps":23.33}, +{"id":40,"name":"Blizzard","type":"Ice","damage":100,"duration":3900,"energy":100,"dps":25.64}, +{"id":131,"name":"Body Slam","type":"Normal","damage":40,"duration":1560,"energy":50,"dps":25.64}, +{"id":22,"name":"Megahorn","type":"Bug","damage":80,"duration":3200,"energy":100,"dps":25.0}, +{"id":122,"name":"Hurricane","type":"Flying","damage":80,"duration":3200,"energy":100,"dps":25.0}, +{"id":116,"name":"Solar Beam","type":"Grass","damage":120,"duration":4900,"energy":100,"dps":24.48}, +{"id":103,"name":"Fire Blast","type":"Fire","damage":100,"duration":4100,"energy":100,"dps":24.39}, +{"id":14,"name":"Hyper Beam","type":"Normal","damage":120,"duration":5000,"energy":100,"dps":24.0}, +{"id":31,"name":"Earthquake","type":"Ground","damage":100,"duration":4200,"energy":100,"dps":23.8}, +{"id":118,"name":"Power Whip","type":"Grass","damage":70,"duration":2800,"energy":100,"dps":25.0}, +{"id":107,"name":"Hydro Pump","type":"Water","damage":90,"duration":3800,"energy":100,"dps":23.68}, +{"id":117,"name":"Leaf Blade","type":"Grass","damage":55,"duration":2800,"energy":50,"dps":19.64}, +{"id":78,"name":"Thunder","type":"Electric","damage":100,"duration":4300,"energy":100,"dps":23.25}, +{"id":123,"name":"Brick Break","type":"Fighting","damage":30,"duration":1600,"energy":33,"dps":18.75}, +{"id":92,"name":"Gunk Shot","type":"Poison","damage":65,"duration":3000,"energy":100,"dps":21.66}, +{"id":90,"name":"Sludge Bomb","type":"Poison","damage":55,"duration":2600,"energy":50,"dps":21.15}, +{"id":42,"name":"Heat Wave","type":"Fire","damage":80,"duration":3800,"energy":100,"dps":21.05}, +{"id":87,"name":"Moonblast","type":"Fairy","damage":85,"duration":4100,"energy":100,"dps":20.73}, +{"id":91,"name":"Sludge Wave","type":"Poison","damage":70,"duration":3400,"energy":100,"dps":20.58}, +{"id":79,"name":"Thunderbolt","type":"Electric","damage":55,"duration":2700,"energy":50,"dps":20.37}, +{"id":47,"name":"Petal Blizzard","type":"Grass","damage":65,"duration":3200,"energy":50,"dps":20.31}, +{"id":89,"name":"Cross Poison","type":"Poison","damage":25,"duration":1500,"energy":25,"dps":16.66}, +{"id":108,"name":"Psychic","type":"Psychic","damage":55,"duration":2800,"energy":50,"dps":19.64}, +{"id":58,"name":"Aqua Tail","type":"Water","damage":45,"duration":2350,"energy":50,"dps":19.14}, +{"id":24,"name":"Flamethrower","type":"Fire","damage":55,"duration":2900,"energy":50,"dps":18.96}, +{"id":88,"name":"Play Rough","type":"Fairy","damage":55,"duration":2900,"energy":50,"dps":18.96}, +{"id":82,"name":"Dragon Pulse","type":"Dragon","damage":65,"duration":3600,"energy":50,"dps":18.05}, +{"id":39,"name":"Ice Beam","type":"Ice","damage":65,"duration":3650,"energy":50,"dps":17.8}, +{"id":49,"name":"Bug Buzz","type":"Bug","damage":75,"duration":4250,"energy":50,"dps":17.64}, +{"id":46,"name":"Drill Run","type":"Ground","damage":50,"duration":3400,"energy":33,"dps":14.7}, +{"id":59,"name":"Seed Bomb","type":"Grass","damage":40,"duration":2400,"energy":33,"dps":16.66}, +{"id":77,"name":"Thunder Punch","type":"Electric","damage":40,"duration":2400,"energy":33,"dps":16.66}, +{"id":100,"name":"X Scissor","type":"Bug","damage":35,"duration":2100,"energy":33,"dps":16.66}, +{"id":129,"name":"Hyper Fang","type":"Normal","damage":35,"duration":2100,"energy":33,"dps":16.66}, +{"id":64,"name":"Rock Slide","type":"Rock","damage":50,"duration":3200,"energy":33,"dps":15.62}, +{"id":94,"name":"Bone Club","type":"Ground","damage":25,"duration":1600,"energy":25,"dps":15.62}, +{"id":36,"name":"Flash Cannon","type":"Steel","damage":60,"duration":3900,"energy":33,"dps":15.38}, +{"id":74,"name":"Iron Head","type":"Steel","damage":30,"duration":2000,"energy":33,"dps":15.0}, +{"id":38,"name":"Drill Peck","type":"Flying","damage":40,"duration":2700,"energy":33,"dps":14.81}, +{"id":60,"name":"Psyshock","type":"Psychic","damage":40,"duration":2700,"energy":33,"dps":14.81}, +{"id":70,"name":"Shadow Ball","type":"Ghost","damage":45,"duration":3080,"energy":33,"dps":14.61}, +{"id":99,"name":"Signal Beam","type":"Bug","damage":45,"duration":3100,"energy":33,"dps":14.51}, +{"id":115,"name":"Fire Punch","type":"Fire","damage":40,"duration":2800,"energy":33,"dps":14.28}, +{"id":54,"name":"Submission","type":"Fighting","damage":30,"duration":2100,"energy":33,"dps":14.28}, +{"id":102,"name":"Flame Burst","type":"Fire","damage":30,"duration":2100,"energy":25,"dps":14.28}, +{"id":127,"name":"Stomp","type":"Normal","damage":30,"duration":2100,"energy":25,"dps":14.28}, +{"id":35,"name":"Discharge","type":"Electric","damage":35,"duration":2500,"energy":33,"dps":14.0}, +{"id":65,"name":"Power Gem","type":"Rock","damage":40,"duration":2900,"energy":33,"dps":13.79}, +{"id":106,"name":"Scald","type":"Water","damage":55,"duration":4000,"energy":33,"dps":13.75}, +{"id":109,"name":"Psystrike","type":"Psychic","damage":70,"duration":5100,"energy":100,"dps":13.72}, +{"id":56,"name":"Low Sweep","type":"Fighting","damage":30,"duration":2250,"energy":25,"dps":13.33}, +{"id":51,"name":"Night Slash","type":"Dark","damage":30,"duration":2700,"energy":25,"dps":11.11}, +{"id":86,"name":"Dazzling Gleam","type":"Fairy","damage":55,"duration":4200,"energy":33,"dps":13.09}, +{"id":16,"name":"Dark Pulse","type":"Dark","damage":45,"duration":3500,"energy":33,"dps":12.85}, +{"id":33,"name":"Ice Punch","type":"Ice","damage":45,"duration":3500,"energy":33,"dps":12.85}, +{"id":26,"name":"Dig","type":"Ground","damage":70,"duration":5800,"energy":33,"dps":12.06}, +{"id":20,"name":"Vice Grip","type":"Normal","damage":25,"duration":2100,"energy":20,"dps":11.9}, +{"id":18,"name":"Sludge","type":"Poison","damage":30,"duration":2600,"energy":25,"dps":11.53}, +{"id":96,"name":"Mud Bomb","type":"Ground","damage":30,"duration":2600,"energy":25,"dps":11.53}, +{"id":126,"name":"Horn Attack","type":"Normal","damage":25,"duration":2200,"energy":25,"dps":11.36}, +{"id":121,"name":"Air Cutter","type":"Flying","damage":30,"duration":3300,"energy":25,"dps":9.09}, +{"id":132,"name":"Rest","type":"Normal","damage":35,"duration":3100,"energy":33,"dps":11.29}, +{"id":72,"name":"Magnet Bomb","type":"Steel","damage":30,"duration":2800,"energy":25,"dps":10.71}, +{"id":57,"name":"Aqua Jet","type":"Water","damage":25,"duration":2350,"energy":20,"dps":10.63}, +{"id":105,"name":"Water Pulse","type":"Water","damage":35,"duration":3300,"energy":25,"dps":10.6}, +{"id":30,"name":"Psybeam","type":"Psychic","damage":40,"duration":3800,"energy":25,"dps":10.52}, +{"id":63,"name":"Rock Tomb","type":"Rock","damage":30,"duration":3400,"energy":25,"dps":8.82}, +{"id":50,"name":"Poison Fang","type":"Poison","damage":25,"duration":2400,"energy":20,"dps":10.41}, +{"id":104,"name":"Brine","type":"Water","damage":25,"duration":2400,"energy":25,"dps":10.41}, +{"id":45,"name":"Aerial Ace","type":"Flying","damage":30,"duration":2900,"energy":25,"dps":10.34}, +{"id":53,"name":"Bubble Beam","type":"Water","damage":30,"duration":2900,"energy":25,"dps":10.34}, +{"id":95,"name":"Bulldoze","type":"Ground","damage":35,"duration":3400,"energy":25,"dps":10.29}, +{"id":125,"name":"Swift","type":"Normal","damage":30,"duration":3000,"energy":25,"dps":10.0}, +{"id":62,"name":"Ancient Power","type":"Rock","damage":35,"duration":3600,"energy":25,"dps":9.72}, +{"id":114,"name":"Giga Drain","type":"Grass","damage":35,"duration":3600,"energy":33,"dps":9.72}, +{"id":69,"name":"Ominous Wind","type":"Ghost","damage":30,"duration":3100,"energy":25,"dps":9.67}, +{"id":67,"name":"Shadow Punch","type":"Ghost","damage":20,"duration":2100,"energy":25,"dps":9.52}, +{"id":80,"name":"Twister","type":"Dragon","damage":25,"duration":2700,"energy":20,"dps":9.25}, +{"id":85,"name":"Draining Kiss","type":"Fairy","damage":25,"duration":2800,"energy":20,"dps":8.92}, +{"id":21,"name":"Flame Wheel","type":"Fire","damage":40,"duration":4600,"energy":25,"dps":8.69}, +{"id":133,"name":"Struggle","type":"Normal","damage":15,"duration":1695,"energy":20,"dps":8.84}, +{"id":101,"name":"Flame Charge","type":"Fire","damage":25,"duration":3100,"energy":20,"dps":8.06}, +{"id":34,"name":"Heart Stamp","type":"Psychic","damage":20,"duration":2550,"energy":25,"dps":7.84}, +{"id":75,"name":"Parabolic Charge","type":"Electric","damage":15,"duration":2100,"energy":20,"dps":7.14}, +{"id":111,"name":"Icy Wind","type":"Ice","damage":25,"duration":3800,"energy":20,"dps":6.57}, +{"id":84,"name":"Disarming Voice","type":"Fairy","damage":25,"duration":3900,"energy":20,"dps":6.41}, +{"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":4000,"energy":20,"dps":6.25}, +{"id":66,"name":"Shadow Sneak","type":"Ghost","damage":15,"duration":3100,"energy":20,"dps":4.83}, +{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] diff --git a/data/fast_moves.json b/data/fast_moves.json new file mode 100644 index 0000000000..ec16d2cc8a --- /dev/null +++ b/data/fast_moves.json @@ -0,0 +1,41 @@ +[{"id":222,"name":"Pound","type":"Normal","damage":7,"duration":540,"energy":7,"dps":12.96}, +{"id":228,"name":"Metal Claw","type":"Steel","damage":8,"duration":630,"energy":7,"dps":12.69}, +{"id":226,"name":"Psycho Cut","type":"Psychic","damage":7,"duration":570,"energy":7,"dps":12.28}, +{"id":210,"name":"Wing Attack","type":"Flying","damage":9,"duration":750,"energy":7,"dps":12.0}, +{"id":202,"name":"Bite","type":"Dark","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":204,"name":"Dragon Breath","type":"Dragon","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":220,"name":"Scratch","type":"Normal","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":230,"name":"Water Gun","type":"Water","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":240,"name":"Fire Fang","type":"Fire","damage":10,"duration":840,"energy":4,"dps":11.9}, +{"id":213,"name":"Shadow Claw","type":"Ghost","damage":11,"duration":950,"energy":7,"dps":11.57}, +{"id":238,"name":"Feint Attack","type":"Dark","damage":12,"duration":1040,"energy":7,"dps":11.53}, +{"id":224,"name":"Poison Jab","type":"Poison","damage":12,"duration":1050,"energy":7,"dps":11.42}, +{"id":234,"name":"Zen Headbutt","type":"Psychic","damage":12,"duration":1050,"energy":4,"dps":11.42}, +{"id":239,"name":"Steel Wing","type":"Steel","damage":15,"duration":1330,"energy":4,"dps":11.27}, +{"id":201,"name":"Bug Bite","type":"Bug","damage":5,"duration":450,"energy":7,"dps":11.11}, +{"id":218,"name":"Frost Breath","type":"Ice","damage":9,"duration":810,"energy":7,"dps":11.11}, +{"id":233,"name":"Mud Slap","type":"Ground","damage":15,"duration":1350,"energy":9,"dps":11.11}, +{"id":216,"name":"Mud Shot","type":"Ground","damage":6,"duration":550,"energy":7,"dps":10.9}, +{"id":221,"name":"Tackle","type":"Normal","damage":12,"duration":1100,"energy":7,"dps":10.9}, +{"id":237,"name":"Bubble","type":"Water","damage":25,"duration":2300,"energy":15,"dps":10.86}, +{"id":214,"name":"Vine Whip","type":"Grass","damage":7,"duration":650,"energy":7,"dps":10.76}, +{"id":217,"name":"Ice Shard","type":"Ice","damage":15,"duration":1400,"energy":7,"dps":10.71}, +{"id":241,"name":"Rock Smash","type":"Fighting","damage":15,"duration":1410,"energy":7,"dps":10.63}, +{"id":223,"name":"Cut","type":"Normal","damage":12,"duration":1130,"energy":7,"dps":10.61}, +{"id":236,"name":"Poison Sting","type":"Poison","damage":6,"duration":575,"energy":4,"dps":10.43}, +{"id":215,"name":"Razor Leaf","type":"Grass","damage":15,"duration":1450,"energy":7,"dps":10.34}, +{"id":212,"name":"Lick","type":"Ghost","damage":5,"duration":500,"energy":7,"dps":10.0}, +{"id":206,"name":"Spark","type":"Electric","damage":7,"duration":700,"energy":4,"dps":10.0}, +{"id":203,"name":"Sucker Punch","type":"Dark","damage":7,"duration":700,"energy":4,"dps":10.0}, +{"id":235,"name":"Confusion","type":"Psychic","damage":15,"duration":1510,"energy":7,"dps":9.93}, +{"id":225,"name":"Acid","type":"Poison","damage":10,"duration":1050,"energy":7,"dps":9.52}, +{"id":209,"name":"Ember","type":"Fire","damage":10,"duration":1050,"energy":7,"dps":9.52}, +{"id":227,"name":"Rock Throw","type":"Rock","damage":12,"duration":1360,"energy":7,"dps":8.82}, +{"id":211,"name":"Peck","type":"Flying","damage":10,"duration":1150,"energy":10,"dps":8.69}, +{"id":207,"name":"Low Kick","type":"Fighting","damage":5,"duration":600,"energy":7,"dps":8.33}, +{"id":205,"name":"Thunder Shock","type":"Electric","damage":5,"duration":600,"energy":7,"dps":8.33}, +{"id":229,"name":"Bullet Punch","type":"Steel","damage":10,"duration":1200,"energy":7,"dps":8.33}, +{"id":219,"name":"Quick Attack","type":"Normal","damage":10,"duration":1330,"energy":7,"dps":7.51}, +{"id":200,"name":"Fury Cutter","type":"Bug","damage":3,"duration":400,"energy":12,"dps":7.5}, +{"id":208,"name":"Karate Chop","type":"Fighting","damage":6,"duration":800,"energy":7,"dps":7.5}, +{"id":231,"name":"Splash","type":"Water","damage":0,"duration":1230,"energy":7,"dps":0.0}] \ No newline at end of file diff --git a/data/level_to_cpm.json b/data/level_to_cpm.json new file mode 100644 index 0000000000..d2483d9a41 --- /dev/null +++ b/data/level_to_cpm.json @@ -0,0 +1,81 @@ +{ + "1": 0.094, + "1.5": 0.135137432, + "2": 0.16639787, + "2.5": 0.192650919, + "3": 0.21573247, + "3.5": 0.236572661, + "4": 0.25572005, + "4.5": 0.273530381, + "5": 0.29024988, + "5.5": 0.306057377, + "6": 0.3210876, + "6.5": 0.335445036, + "7": 0.34921268, + "7.5": 0.362457751, + "8": 0.37523559, + "8.5": 0.387592406, + "9": 0.39956728, + "9.5": 0.411193551, + "10": 0.42250001, + "10.5": 0.432926419, + "11": 0.44310755, + "11.5": 0.4530599578, + "12": 0.46279839, + "12.5": 0.472336083, + "13": 0.48168495, + "13.5": 0.4908558, + "14": 0.49985844, + "14.5": 0.508701765, + "15": 0.51739395, + "15.5": 0.525942511, + "16": 0.53435433, + "16.5": 0.542635767, + "17": 0.55079269, + "17.5": 0.558830576, + "18": 0.56675452, + "18.5": 0.574569153, + "19": 0.58227891, + "19.5": 0.589887917, + "20": 0.59740001, + "20.5": 0.604818814, + "21": 0.61215729, + "21.5": 0.619399365, + "22": 0.62656713, + "22.5": 0.633644533, + "23": 0.64065295, + "23.5": 0.647576426, + "24": 0.65443563, + "24.5": 0.661214806, + "25": 0.667934, + "25.5": 0.674577537, + "26": 0.68116492, + "26.5": 0.687680648, + "27": 0.69414365, + "27.5": 0.700538673, + "28": 0.70688421, + "28.5": 0.713164996, + "29": 0.71939909, + "29.5": 0.725571552, + "30": 0.7317, + "30.5": 0.734741009, + "31": 0.73776948, + "31.5": 0.740785574, + "32": 0.74378943, + "32.5": 0.746781211, + "33": 0.74976104, + "33.5": 0.752729087, + "34": 0.75568551, + "34.5": 0.758630378, + "35": 0.76156384, + "35.5": 0.764486065, + "36": 0.76739717, + "36.5": 0.770297266, + "37": 0.7731865, + "37.5": 0.776064962, + "38": 0.77893275, + "38.5": 0.781790055, + "39": 0.78463697, + "39.5": 0.787473578, + "40": 0.79030001 +} \ No newline at end of file diff --git a/data/pokemon.json b/data/pokemon.json index a227106841..44a22a9fd0 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -1 +1,5900 @@ -[{"Number":"001","Name":"Bulbasaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Tackle","Vine Whip"],"Weight":"6.9 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"002","Name":"Ivysaur"},{"Number":"003","Name":"Venusaur"}]},{"Number":"002","Name":"Ivysaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"13.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"003","Name":"Venusaur"}]},{"Number":"003","Name":"Venusaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"100.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"},{"Number":"002","Name":"Ivysaur"}]},{"Number":"004","Name":"Charmander","Classification":"Lizard Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Scratch"],"Weight":"8.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"005","Name":"Charmeleon"},{"Number":"006","Name":"Charizard"}]},{"Number":"005","Name":"Charmeleon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"19.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"}],"Next Evolution Requirements":{"Amount":100,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"006","Name":"Charizard"}]},{"Number":"006","Name":"Charizard","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Ember","Wing Attack"],"Weight":"90.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"},{"Number":"005","Name":"Charmeleon"}]},{"Number":"007","Name":"Squirtle","Classification":"Tiny Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Tackle","Bubble"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"008","Name":"Wartortle"},{"Number":"009","Name":"Blastoise"}]},{"Number":"008","Name":"Wartortle","Classification":"Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"22.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"}],"Next Evolution Requirements":{"Amount":100,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"009","Name":"Blastoise"}]},{"Number":"009","Name":"Blastoise","Classification":"Shellfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"85.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"},{"Number":"008","Name":"Wartortle"}]},{"Number":"010","Name":"Caterpie","Classification":"Worm Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"2.9 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"011","Name":"Metapod"},{"Number":"012","Name":"Butterfree"}]},{"Number":"011","Name":"Metapod","Classification":"Cocoon Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"9.9 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"}],"Next Evolution Requirements":{"Amount":50,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"012","Name":"Butterfree"}]},{"Number":"012","Name":"Butterfree","Classification":"Butterfly Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"32.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"},{"Number":"011","Name":"Metapod"}]},{"Number":"013","Name":"Weedle","Classification":"Hairy Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Sting"],"Weight":"3.2 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"014","Name":"Kakuna"},{"Number":"015","Name":"Beedrill"}]},{"Number":"014","Name":"Kakuna","Classification":"Cocoon Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Posion Sting"],"Weight":"10.0 kg","Height":"0.6 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"}],"Next Evolution Requirements":{"Amount":50,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"015","Name":"Beedrill"}]},{"Number":"015","Name":"Beedrill","Classification":"Poison Bee Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Jab"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"},{"Number":"014","Name":"Kakuna"}]},{"Number":"016","Name":"Pidgey","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"1.8 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"017","Name":"Pidgeotto"},{"Number":"018","Name":"Pidgeot"}]},{"Number":"017","Name":"Pidgeotto","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"30.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"}],"Next Evolution Requirements":{"Amount":50,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"018","Name":"Pidgeot"}]},{"Number":"018","Name":"Pidgeot","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Hurricane"],"Weight":"39.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"},{"Number":"017","Name":"Pidgeotto"}]},{"Number":"019","Name":"Rattata","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Body Slam","Dig","Hyper Fang"],"Weight":"3.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Rattata candies"},"Next evolution(s)":[{"Number":"020","Name":"Raticate"}]},{"Number":"020","Name":"Raticate","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Quick Attack"],"Special Attack(s)":["Dig","Hyper Beam","Hyper Fang"],"Weight":"18.5 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"019","Name":"Rattata"}]},{"Number":"021","Name":"Spearow","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"2.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Spearow candies"},"Next evolution(s)":[{"Number":"022","Name":"Fearow"}]},{"Number":"022","Name":"Fearow","Classification":"Beak Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Steel Wing"],"Weight":"38.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"021","Name":"Spearow"}]},{"Number":"023","Name":"Ekans","Classification":"Snake Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Sting"],"Weight":"6.9 kg","Height":"2.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ekans candies"},"Next evolution(s)":[{"Number":"024","Name":"Arbok"}]},{"Number":"024","Name":"Arbok","Classification":"Cobra Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Bite"],"Weight":"65.0 kg","Height":"3.5 m","Previous evolution(s)":[{"Number":"023","Name":"Ekans"}]},{"Number":"025","Name":"Pikachu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Quick Attack","Thunder Shock"],"Weight":"6.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Pikachu candies"},"Next evolution(s)":[{"Number":"026","Name":"Raichu"}]},{"Number":"026","Name":"Raichu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock","Spark"],"Weight":"30.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"025","Name":"Pikachu"}]},{"Number":"027","Name":"Sandshrew","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"12.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Sandshrew candies"},"Next evolution(s)":[{"Number":"028","Name":"Sandslash"}]},{"Number":"028","Name":"Sandslash","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"027","Name":"Sandshrew"}]},{"Number":"029","Name":"Nidoran F","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"7.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"030","Name":"Nidorina"},{"Number":"031","Name":"Nidoqueen"}]},{"Number":"030","Name":"Nidorina","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"20.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"031","Name":"Nidoqueen"}]},{"Number":"031","Name":"Nidoqueen","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"},{"Number":"030","Name":"Nidorina"}]},{"Number":"032","Name":"Nidoran M","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Peck","Poison Sting"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"033","Name":"Nidorino"},{"Number":"034","Name":"Nidoking"}]},{"Number":"033","Name":"Nidorino","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"19.5 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"034","Name":"Nidoking"}]},{"Number":"034","Name":"Nidoking","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Fury Cutter","Poison Jab"],"Weight":"62.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"},{"Number":"033","Name":"Nidorino"}]},{"Number":"035","Name":"Clefairy","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"7.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Clefairy candies"},"Next evolution(s)":[{"Number":"036","Name":"Clefable"}]},{"Number":"036","Name":"Clefable","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"40.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"035","Name":"Clefairy"}]},{"Number":"037","Name":"Vulpix","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"9.9 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Vulpix candies"},"Next evolution(s)":[{"Number":"038","Name":"Ninetales"}]},{"Number":"038","Name":"Ninetales","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"19.9 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"037","Name":"Vulpix"}]},{"Number":"039","Name":"Jigglypuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"5.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Jigglypuff candies"},"Next evolution(s)":[{"Number":"039","Name":"Jigglypuff"}]},{"Number":"040","Name":"Wigglytuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"12.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"040","Name":"Wigglytuff"}]},{"Number":"041","Name":"Zubat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Quick Attack"],"Weight":"7.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Zubat candies"},"Next evolution(s)":[{"Number":"042","Name":"Golbat"}]},{"Number":"042","Name":"Golbat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Wing Attack"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"041","Name":"Zubat"}]},{"Number":"043","Name":"Oddish","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"5.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"044","Name":"Gloom"},{"Number":"045","Name":"Vileplume"}]},{"Number":"044","Name":"Gloom","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"8.6 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"}],"Next Evolution Requirements":{"Amount":100,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"045","Name":"Vileplume"}]},{"Number":"045","Name":"Vileplume","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid",""],"Weight":"18.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"},{"Number":"044","Name":"Gloom"}]},{"Number":"046","Name":"Paras","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Scratch"],"Weight":"5.4 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Paras candies"},"Next evolution(s)":[{"Number":"047","Name":"Parasect"}]},{"Number":"047","Name":"Parasect","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Fury Cutter"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"046","Name":"Paras"}]},{"Number":"048","Name":"Venonat","Classification":"Insect Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Venonat candies"},"Next evolution(s)":[{"Number":"049","Name":"Venomoth"}]},{"Number":"049","Name":"Venomoth","Classification":"Poison Moth Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"12.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"048","Name":"Venonat"}]},{"Number":"050","Name":"Diglett","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"0.8 kg","Height":"0.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Diglett candies"},"Next evolution(s)":[{"Number":"051","Name":"Dugtrio"}]},{"Number":"051","Name":"Dugtrio","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Sucker Punch"],"Weight":"33.3 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"050","Name":"Diglett"}]},{"Number":"052","Name":"Meowth","Classification":"Scratch Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Scratch"],"Weight":"4.2 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Meowth candies"},"Next evolution(s)":[{"Number":"053","Name":"Persian"}]},{"Number":"053","Name":"Persian","Classification":"Classy Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Scratch"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"052","Name":"Meowth"}]},{"Number":"054","Name":"Psyduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun","Zen Headbutt"],"Weight":"19.6 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Psyduck candies"},"Next evolution(s)":[{"Number":"055","Name":"Golduck"}]},{"Number":"055","Name":"Golduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"76.6 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"054","Name":"Psyduck"}]},{"Number":"056","Name":"Mankey","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Scratch"],"Weight":"28.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Mankey candies"},"Next evolution(s)":[{"Number":"057","Name":"Primeape"}]},{"Number":"057","Name":"Primeape","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"056","Name":"Mankey"}]},{"Number":"058","Name":"Growlithe","Classification":"Puppy Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Ember"],"Weight":"19.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":50,"Name":"Growlithe candies"},"Next evolution(s)":[{"Number":"059","Name":"Arcanine"}]},{"Number":"059","Name":"Arcanine","Classification":"Legendary Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Fire Fang"],"Weight":"155.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"058","Name":"Growlithe"}]},{"Number":"060","Name":"Poliwag","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"12.4 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"061","Name":"Poliwhirl"},{"Number":"062","Name":"Poliwrath"}]},{"Number":"061","Name":"Poliwhirl","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"20.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"}],"Next Evolution Requirements":{"Amount":100,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"062","Name":"Poliwrath"}]},{"Number":"062","Name":"Poliwrath","Classification":"Tadpole Pokemon","Type I":["Water"],"Type II":["Fighting"],"Weaknesses":["Electric","Grass","Flying","Psychic","Fairy"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"54.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"},{"Number":"061","Name":"Poliwhirl"}]},{"Number":"063","Name":"Abra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Zen Headbutt",""],"Weight":"19.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":25,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"064","Name":"Kadabra"},{"Number":"065","Name":"Alakazam"}]},{"Number":"064","Name":"Kadabra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"56.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"}],"Next Evolution Requirements":{"Amount":100,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"065","Name":"Alakazam"}]},{"Number":"065","Name":"Alakazam","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"48.0 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"},{"Number":"064","Name":"Kadabra"}]},{"Number":"066","Name":"Machop","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"19.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"067","Name":"Machoke"},{"Number":"068","Name":"Machamp"}]},{"Number":"067","Name":"Machoke","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"70.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"}],"Next Evolution Requirements":{"Amount":100,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"068","Name":"Machamp"}]},{"Number":"068","Name":"Machamp","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Karate Chop"],"Weight":"130.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"},{"Number":"067","Name":"Machoke"}]},{"Number":"069","Name":"Bellsprout","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Vine Whip"],"Weight":"4.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"070","Name":"Weepinbell"},{"Number":"071","Name":"Victreebel"}]},{"Number":"070","Name":"Weepinbell","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"6.4 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"071","Name":"Victreebel"}]},{"Number":"071","Name":"Victreebel","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"15.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"},{"Number":"070","Name":"Weepinbell"}]},{"Number":"072","Name":"Tentacool","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Bubble","Poison Sting"],"Weight":"45.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Tentacool candies"},"Next evolution(s)":[{"Number":"073","Name":"Tentacruel"}]},{"Number":"073","Name":"Tentacruel","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Jab"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"072","Name":"Tentacool"}]},{"Number":"074","Name":"Geodude","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"20.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"075","Name":"Graveler"},{"Number":"076","Name":"Golem"}]},{"Number":"075","Name":"Graveler","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"105.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"}],"Next Evolution Requirements":{"Amount":100,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"076","Name":"Golem"}]},{"Number":"076","Name":"Golem","Classification":"Megaton Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"300.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"},{"Number":"075","Name":"Graveler"}]},{"Number":"077","Name":"Ponyta","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Tackle"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ponyta candies"},"Next evolution(s)":[{"Number":"078","Name":"Rapidash"}]},{"Number":"078","Name":"Rapidash","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Low Kick"],"Weight":"95.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"077","Name":"Ponyta"}]},{"Number":"079","Name":"Slowpoke","Classification":"Dopey Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"36.0 kg","Height":"1.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Slowpoke candies"},"Next evolution(s)":[{"Number":"080","Name":"Slowbro"}]},{"Number":"080","Name":"Slowbro","Classification":"Hermit Crab Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"78.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"079","Name":"Slowpoke"}]},{"Number":"081","Name":"Magnemite","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"6.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Magnemite candies"},"Next evolution(s)":[{"Number":"082","Name":"Magneton"}]},{"Number":"082","Name":"Magneton","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"60.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"081","Name":"Magnemite"}]},{"Number":"083","Name":"Farfetch'd","Classification":"Wild Duck Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"15.0 kg","Height":"0.8 m"},{"Number":"084","Name":"Doduo","Classification":"Twin Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"39.2 kg","Height":"1.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Doduo candies"},"Next evolution(s)":[{"Number":"085","Name":"Dodrio"}]},{"Number":"085","Name":"Dodrio","Classification":"Triple Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Feint Attack","Steel Wing"],"Weight":"85.2 kg","Height":"1.8 m","Previous evolution(s)":[{"Number":"084","Name":"Doduo"}]},{"Number":"086","Name":"Seel","Classification":"Sea Lion Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Water Gun"],"Weight":"90.0 kg","Height":"1.1 m","Next Evolution Requirements":{"Amount":50,"Name":"Seel candies"},"Next evolution(s)":[{"Number":"087","Name":"Dewgong"}]},{"Number":"087","Name":"Dewgong","Classification":"Sea Lion Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"120.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"086","Name":"Seel"}]},{"Number":"088","Name":"Grimer","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Mud Slap"],"Weight":"30.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Grimer candies"},"Next evolution(s)":[{"Number":"089","Name":"Muk"}]},{"Number":"089","Name":"Muk","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Poison Jab",""],"Weight":"30.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"088","Name":"Grimer"}]},{"Number":"090","Name":"Shellder","Classification":"Bivalve Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Tackle"],"Weight":"4.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Shellder candies"},"Next evolution(s)":[{"Number":"091","Name":"Cloyster"}]},{"Number":"091","Name":"Cloyster","Classification":"Bivalve Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"132.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"090","Name":"Shellder"}]},{"Number":"092","Name":"Gastly","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Sucker Punch"],"Weight":"0.1 kg","Height":"1.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"093","Name":"Haunter"},{"Number":"094","Name":"Gengar"}]},{"Number":"093","Name":"Haunter","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Shadow Claw"],"Weight":"0.1 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"}],"Next Evolution Requirements":{"Amount":100,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"094","Name":"Gengar"}]},{"Number":"094","Name":"Gengar","Classification":"Shadow Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Shadow Claw","Sucker Punch"],"Weight":"40.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"},{"Number":"093","Name":"Haunter"}]},{"Number":"095","Name":"Onix","Classification":"Rock Snake Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"210.0 kg","Height":"8.8 m"},{"Number":"096","Name":"Drowzee","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Pound"],"Weight":"32.4 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Drowzee candies"},"Next evolution(s)":[{"Number":"097","Name":"Hypno"}]},{"Number":"097","Name":"Hypno","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"75.6 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"096","Name":"Drowzee"}]},{"Number":"098","Name":"Krabby","Classification":"River Crab Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Krabby candies"},"Next evolution(s)":[{"Number":"099","Name":"Kingler"}]},{"Number":"099","Name":"Kingler","Classification":"Pincer Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"098","Name":"Krabby"}]},{"Number":"100","Name":"Voltorb","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark","Tackle"],"Weight":"10.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Voltorb candies"},"Next evolution(s)":[{"Number":"101","Name":"Electrode"}]},{"Number":"101","Name":"Electrode","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark",""],"Weight":"66.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"100","Name":"Voltorb"}]},{"Number":"102","Name":"Exeggcute","Classification":"Egg Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion",""],"Weight":"2.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Exeggcute candies"},"Next evolution(s)":[{"Number":"103","Name":"Exeggutor"}]},{"Number":"103","Name":"Exeggutor","Classification":"Coconut Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"120.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"102","Name":"Exeggcute"}]},{"Number":"104","Name":"Cubone","Classification":"Lonely Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Cubone candies"},"Next evolution(s)":[{"Number":"105","Name":"Marowak"}]},{"Number":"105","Name":"Marowak","Classification":"Bone Keeper Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"45.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"104","Name":"Cubone"}]},{"Number":"106","Name":"Hitmonlee","Classification":"Kicking Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Low Kick","Rock Smash"],"Weight":"49.8 kg","Height":"1.5 m","Next evolution(s)":[{"Number":"107","Name":"Hitmonchan"}]},{"Number":"107","Name":"Hitmonchan","Classification":"Punching Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Rock Smash"],"Weight":"50.2 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"106","Name":"Hitmonlee"}]},{"Number":"108","Name":"Lickitung","Classification":"Licking Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"65.5 kg","Height":"1.2 m"},{"Number":"109","Name":"Koffing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"1.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Koffing candies"},"Next evolution(s)":[{"Number":"110","Name":"Weezing"}]},{"Number":"110","Name":"Weezing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"9.5 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"109","Name":"Koffing"}]},{"Number":"111","Name":"Rhyhorn","Classification":"Spikes Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"115.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Rhyhorn candies"},"Next evolution(s)":[{"Number":"112","Name":"Rhydon"}]},{"Number":"112","Name":"Rhydon","Classification":"Drill Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"120.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"111","Name":"Rhyhorn"}]},{"Number":"113","Name":"Chansey","Classification":"Egg Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"34.6 kg","Height":"1.1 m"},{"Number":"114","Name":"Tangela","Classification":"Vine Pokemon","Type I":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug"],"Fast Attack(s)":["Vine Whip",""],"Weight":"35.0 kg","Height":"1.0 m"},{"Number":"115","Name":"Kangaskhan","Classification":"Parent Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Low Kick",""],"Weight":"80.0 kg","Height":"2.2 m"},{"Number":"116","Name":"Horsea","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Water Gun"],"Weight":"8.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Horsea candies"},"Next evolution(s)":[{"Number":"117","Name":"Seadra"}]},{"Number":"117","Name":"Seadra","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Dragon Breath","Water Gun"],"Weight":"25.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"116","Name":"Horsea"}]},{"Number":"118","Name":"Goldeen","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Mud Shot"],"Weight":"15.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Goldeen candies"},"Next evolution(s)":[{"Number":"119","Name":"Seaking"}]},{"Number":"119","Name":"Seaking","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Poison Jab"],"Weight":"39.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"118","Name":"Goldeen"}]},{"Number":"120","Name":"Staryu","Classification":"Starshape Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"34.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Staryu candies"},"Next evolution(s)":[{"Number":"120","Name":"Staryu"}]},{"Number":"121","Name":"Starmie","Classification":"Mysterious Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"80.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"121","Name":"Starmie"}]},{"Number":"122","Name":"Mr. Mime","Classification":"Barrier Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"54.5 kg","Height":"1.3 m"},{"Number":"123","Name":"Scyther","Classification":"Mantis Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Steel Wing"],"Weight":"56.0 kg","Height":"1.5 m"},{"Number":"124","Name":"Jynx","Classification":"Humanshape Pokemon","Type I":["Ice"],"Type II":["Psychic"],"Weaknesses":["Fire","Bug","Rock","Ghost","Dark","Steel"],"Fast Attack(s)":["Frost Breath","Pound"],"Weight":"40.6 kg","Height":"1.4 m"},{"Number":"125","Name":"Electabuzz","Classification":"Electric Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Low Kick","Thunder Shock"],"Weight":"30.0 kg","Height":"1.1 m"},{"Number":"126","Name":"Magmar","Classification":"Spitfire Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Karate Chop"],"Weight":"44.5 kg","Height":"1.3 m"},{"Number":"127","Name":"Pinsir","Classification":"Stagbeetle Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Rock Smash"],"Weight":"55.0 kg","Height":"1.5 m"},{"Number":"128","Name":"Tauros","Classification":"Wild Bull Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Tackle","Zen Headbutt"],"Weight":"88.4 kg","Height":"1.4 m"},{"Number":"129","Name":"Magikarp","Classification":"Fish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Splash",""],"Weight":"10.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":400,"Name":"Magikarp candies"},"Next evolution(s)":[{"Number":"130","Name":"Gyarados"}]},{"Number":"130","Name":"Gyarados","Classification":"Atrocious Pokemon","Type I":["Water"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Bite","Dragon Breath"],"Weight":"235.0 kg","Height":"6.5 m","Previous evolution(s)":[{"Number":"129","Name":"Magikarp"}]},{"Number":"131","Name":"Lapras","Classification":"Transport Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"220.0 kg","Height":"2.5 m"},{"Number":"132","Name":"Ditto","Classification":"Transform Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.3 m"},{"Number":"133","Name":"Eevee","Classification":"Evolution Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"6.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Eevee candies"},"Next evolution(s)":[{"Number":"134","Name":"Vaporeon"},{"Number":"135","Name":"Jolteon"},{"Number":"136","Name":"Flareon"}]},{"Number":"134","Name":"Vaporeon","Classification":"Bubble Jet Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun",""],"Weight":"29.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"135","Name":"Jolteon","Classification":"Lightning Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock",""],"Weight":"24.5 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"136","Name":"Flareon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"25.0 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"137","Name":"Porygon","Classification":"Virtual Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"36.5 kg","Height":"0.8 m"},{"Number":"138","Name":"Omanyte","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Water Gun",""],"Weight":"7.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Omanyte candies"},"Next evolution(s)":[{"Number":"139","Name":"Omastar"}]},{"Number":"139","Name":"Omastar","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Rock Throw","Water Gun"],"Weight":"35.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"138","Name":"Omanyte"}]},{"Number":"140","Name":"Kabuto","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"11.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Kabuto candies"},"Next evolution(s)":[{"Number":"141","Name":"Kabutops"}]},{"Number":"141","Name":"Kabutops","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Fury Cutter","Mud Shot"],"Weight":"40.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"140","Name":"Kabuto"}]},{"Number":"142","Name":"Aerodactyl","Classification":"Fossil Pokemon","Type I":["Rock"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Ice","Rock","Steel"],"Fast Attack(s)":["Bite","Steel Wing"],"Weight":"59.0 kg","Height":"1.8 m"},{"Number":"143","Name":"Snorlax","Classification":"Sleeping Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"460.0 kg","Height":"2.1 m"},{"Number":"144","Name":"Articuno","Classification":"Freeze Pokemon","Type I":["Ice"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Rock","Steel"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"55.4 kg","Height":"1.7 m"},{"Number":"145","Name":"Zapdos","Classification":"Electric Pokemon","Type I":["Electric"],"Type II":["Flying"],"Weaknesses":["Ice","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"52.6 kg","Height":"1.6 m"},{"Number":"146","Name":"Moltres","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"60.0 kg","Height":"2.0 m"},{"Number":"147","Name":"Dratini","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"3.3 kg","Height":"1.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Dratini candies"}},{"Number":"148","Name":"Dragonair","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"16.5 kg","Height":"4.0 m","Next Evolution Requirements":{"Amount":100,"Name":"Dratini candies"},"Next evolution(s)":[{"Number":"149","Name":"Dragonite"}]},{"Number":"149","Name":"Dragonite","Classification":"Dragon Pokemon","Type I":["Dragon"],"Type II":["Flying"],"Weaknesses":["Ice","Rock","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath","Steel Wing"],"Weight":"210.0 kg","Height":"2.2 m","Previous evolution(s)":[{"Number":"148","Name":"Dragonair"}]},{"Number":"150","Name":"Mewtwo","Classification":"Genetic Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"122.0 kg","Height":"2.0 m"},{"Number":"151","Name":"Mew","Classification":"New Species Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.4 m"}] \ No newline at end of file +[ + { + "Number": "001", + "Name": "Bulbasaur", + "Classification": "Seed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Tackle", + "Vine Whip" + ], + "Weight": "6.9 kg", + "Height": "0.7 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 1, + "Name": "Bulbasaur candies" + }, + "Next evolution(s)": [ + { + "Number": "002", + "Name": "Ivysaur" + }, + { + "Number": "003", + "Name": "Venusaur" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Seed Bomb", + "Sludge Bomb" + ], + "BaseAttack": 126, + "BaseDefense": 126, + "BaseStamina": 90, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "002", + "Name": "Ivysaur", + "Classification": "Seed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Razor Leaf", + "Vine Whip" + ], + "Weight": "13.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "001", + "Name": "Bulbasaur" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 1, + "Name": "Bulbasaur candies" + }, + "Next evolution(s)": [ + { + "Number": "003", + "Name": "Venusaur" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 156, + "BaseDefense": 158, + "BaseStamina": 120, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "003", + "Name": "Venusaur", + "Classification": "Seed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Razor Leaf", + "Vine Whip" + ], + "Weight": "100.0 kg", + "Height": "2.0 m", + "Previous evolution(s)": [ + { + "Number": "001", + "Name": "Bulbasaur" + }, + { + "Number": "002", + "Name": "Ivysaur" + } + ], + "Special Attack(s)": [ + "Petal Blizzard", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 198, + "BaseDefense": 200, + "BaseStamina": 160, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "004", + "Name": "Charmander", + "Classification": "Lizard Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Scratch" + ], + "Weight": "8.5 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 4, + "Name": "Charmander candies" + }, + "Next evolution(s)": [ + { + "Number": "005", + "Name": "Charmeleon" + }, + { + "Number": "006", + "Name": "Charizard" + } + ], + "Special Attack(s)": [ + "Flame Burst", + "Flame Charge", + "Flamethrower" + ], + "BaseAttack": 128, + "BaseDefense": 108, + "BaseStamina": 78, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "005", + "Name": "Charmeleon", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Scratch" + ], + "Weight": "19.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "004", + "Name": "Charmander" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 4, + "Name": "Charmander candies" + }, + "Next evolution(s)": [ + { + "Number": "006", + "Name": "Charizard" + } + ], + "Special Attack(s)": [ + "Fire Punch", + "Flame Burst", + "Flamethrower" + ], + "BaseAttack": 160, + "BaseDefense": 140, + "BaseStamina": 116, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "006", + "Name": "Charizard", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Water", + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Wing Attack" + ], + "Weight": "90.5 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "004", + "Name": "Charmander" + }, + { + "Number": "005", + "Name": "Charmeleon" + } + ], + "Special Attack(s)": [ + "Dragon Claw", + "Fire Blast", + "Flamethrower" + ], + "BaseAttack": 212, + "BaseDefense": 182, + "BaseStamina": 156, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "007", + "Name": "Squirtle", + "Classification": "Tiny Turtle Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Tackle" + ], + "Weight": "9.0 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 7, + "Name": "Squirtle candies" + }, + "Next evolution(s)": [ + { + "Number": "008", + "Name": "Wartortle" + }, + { + "Number": "009", + "Name": "Blastoise" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Aqua Tail", + "Water Pulse" + ], + "BaseAttack": 112, + "BaseDefense": 142, + "BaseStamina": 88, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "008", + "Name": "Wartortle", + "Classification": "Turtle Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bite", + "Water Gun" + ], + "Weight": "22.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "007", + "Name": "Squirtle" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 7, + "Name": "Squirtle candies" + }, + "Next evolution(s)": [ + { + "Number": "009", + "Name": "Blastoise" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Hydro Pump", + "Ice Beam" + ], + "BaseAttack": 144, + "BaseDefense": 176, + "BaseStamina": 118, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "009", + "Name": "Blastoise", + "Classification": "Shellfish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bite", + "Water Gun" + ], + "Weight": "85.5 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "007", + "Name": "Squirtle" + }, + { + "Number": "008", + "Name": "Wartortle" + } + ], + "Special Attack(s)": [ + "Flash Cannon", + "Hydro Pump", + "Ice Beam" + ], + "BaseAttack": 186, + "BaseDefense": 222, + "BaseStamina": 158, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "010", + "Name": "Caterpie", + "Classification": "Worm Pokemon", + "Type I": [ + "Bug" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Tackle" + ], + "Weight": "2.9 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 12, + "Family": 10, + "Name": "Caterpie candies" + }, + "Next evolution(s)": [ + { + "Number": "011", + "Name": "Metapod" + }, + { + "Number": "012", + "Name": "Butterfree" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 62, + "BaseDefense": 66, + "BaseStamina": 90, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "011", + "Name": "Metapod", + "Classification": "Cocoon Pokemon", + "Type I": [ + "Bug" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Tackle" + ], + "Weight": "9.9 kg", + "Height": "0.7 m", + "Previous evolution(s)": [ + { + "Number": "010", + "Name": "Caterpie" + } + ], + "Next Evolution Requirements": { + "Amount": 50, + "Family": 10, + "Name": "Caterpie candies" + }, + "Next evolution(s)": [ + { + "Number": "012", + "Name": "Butterfree" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 56, + "BaseDefense": 86, + "BaseStamina": 100, + "CaptureRate": 0.2, + "FleeRate": 0.09 + }, + { + "Number": "012", + "Name": "Butterfree", + "Classification": "Butterfly Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Fire", + "Electric", + "Ice", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Confusion" + ], + "Weight": "32.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "010", + "Name": "Caterpie" + }, + { + "Number": "011", + "Name": "Metapod" + } + ], + "Special Attack(s)": [ + "Bug Buzz", + "Psychic", + "Signal Beam" + ], + "BaseAttack": 144, + "BaseDefense": 144, + "BaseStamina": 120, + "CaptureRate": 0.1, + "FleeRate": 0.06 + }, + { + "Number": "013", + "Name": "Weedle", + "Classification": "Hairy Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Poison Sting" + ], + "Weight": "3.2 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 12, + "Family": 13, + "Name": "Weedle candies" + }, + "Next evolution(s)": [ + { + "Number": "014", + "Name": "Kakuna" + }, + { + "Number": "015", + "Name": "Beedrill" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 68, + "BaseDefense": 64, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "014", + "Name": "Kakuna", + "Classification": "Cocoon Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Poison Sting" + ], + "Weight": "10.0 kg", + "Height": "0.6 m", + "Previous evolution(s)": [ + { + "Number": "013", + "Name": "Weedle" + } + ], + "Next Evolution Requirements": { + "Amount": 50, + "Family": 13, + "Name": "Weedle candies" + }, + "Next evolution(s)": [ + { + "Number": "015", + "Name": "Beedrill" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 62, + "BaseDefense": 82, + "BaseStamina": 90, + "CaptureRate": 0.2, + "FleeRate": 0.09 + }, + { + "Number": "015", + "Name": "Beedrill", + "Classification": "Poison Bee Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Poison Jab" + ], + "Weight": "29.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "013", + "Name": "Weedle" + }, + { + "Number": "014", + "Name": "Kakuna" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Sludge Bomb", + "X Scissor" + ], + "BaseAttack": 144, + "BaseDefense": 130, + "BaseStamina": 130, + "CaptureRate": 0.1, + "FleeRate": 0.06 + }, + { + "Number": "016", + "Name": "Pidgey", + "Classification": "Tiny Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Twister" + ], + "Weight": "1.8 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 12, + "Family": 16, + "Name": "Pidgey candies" + }, + "Next evolution(s)": [ + { + "Number": "017", + "Name": "Pidgeotto" + }, + { + "Number": "018", + "Name": "Pidgeot" + } + ], + "BaseAttack": 94, + "BaseDefense": 90, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "017", + "Name": "Pidgeotto", + "Classification": "Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Steel Wing", + "Wing Attack" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Twister" + ], + "Weight": "30.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "016", + "Name": "Pidgey" + } + ], + "Next Evolution Requirements": { + "Amount": 50, + "Family": 16, + "Name": "Pidgey candies" + }, + "Next evolution(s)": [ + { + "Number": "018", + "Name": "Pidgeot" + } + ], + "BaseAttack": 126, + "BaseDefense": 122, + "BaseStamina": 126, + "CaptureRate": 0.2, + "FleeRate": 0.09 + }, + { + "Number": "018", + "Name": "Pidgeot", + "Classification": "Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Steel Wing", + "Wing Attack" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Hurricane" + ], + "Weight": "39.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "016", + "Name": "Pidgey" + }, + { + "Number": "017", + "Name": "Pidgeotto" + } + ], + "BaseAttack": 170, + "BaseDefense": 166, + "BaseStamina": 166, + "CaptureRate": 0.1, + "FleeRate": 0.06 + }, + { + "Number": "019", + "Name": "Rattata", + "Classification": "Mouse Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Special Attack(s)": [ + "Body Slam", + "Dig", + "Hyper Fang" + ], + "Weight": "3.5 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 19, + "Name": "Rattata candies" + }, + "Next evolution(s)": [ + { + "Number": "020", + "Name": "Raticate" + } + ], + "BaseAttack": 92, + "BaseDefense": 86, + "BaseStamina": 60, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "020", + "Name": "Raticate", + "Classification": "Mouse Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Bite", + "Quick Attack" + ], + "Special Attack(s)": [ + "Dig", + "Hyper Beam", + "Hyper Fang" + ], + "Weight": "18.5 kg", + "Height": "0.7 m", + "Previous evolution(s)": [ + { + "Number": "019", + "Name": "Rattata" + } + ], + "BaseAttack": 146, + "BaseDefense": 150, + "BaseStamina": 110, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "021", + "Name": "Spearow", + "Classification": "Tiny Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Peck", + "Quick Attack" + ], + "Weight": "2.0 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 21, + "Name": "Spearow candies" + }, + "Next evolution(s)": [ + { + "Number": "022", + "Name": "Fearow" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Drill Peck", + "Twister" + ], + "BaseAttack": 102, + "BaseDefense": 78, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "022", + "Name": "Fearow", + "Classification": "Beak Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Peck", + "Steel Wing" + ], + "Weight": "38.0 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "021", + "Name": "Spearow" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Drill Run", + "Twister" + ], + "BaseAttack": 168, + "BaseDefense": 146, + "BaseStamina": 130, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "023", + "Name": "Ekans", + "Classification": "Snake Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Poison Sting" + ], + "Weight": "6.9 kg", + "Height": "2.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 23, + "Name": "Ekans candies" + }, + "Next evolution(s)": [ + { + "Number": "024", + "Name": "Arbok" + } + ], + "Special Attack(s)": [ + "Gunk Shot", + "Sludge Bomb", + "Wrap" + ], + "BaseAttack": 112, + "BaseDefense": 112, + "BaseStamina": 70, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "024", + "Name": "Arbok", + "Classification": "Cobra Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Bite" + ], + "Weight": "65.0 kg", + "Height": "3.5 m", + "Previous evolution(s)": [ + { + "Number": "023", + "Name": "Ekans" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Gunk Shot", + "Sludge Wave" + ], + "BaseAttack": 166, + "BaseDefense": 166, + "BaseStamina": 120, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "025", + "Name": "Pikachu", + "Classification": "Mouse Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Thunder Shock" + ], + "Weight": "6.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 25, + "Name": "Pikachu candies" + }, + "Next evolution(s)": [ + { + "Number": "026", + "Name": "Raichu" + } + ], + "Special Attack(s)": [ + "Discharge", + "Thunder", + "Thunderbolt" + ], + "BaseAttack": 124, + "BaseDefense": 108, + "BaseStamina": 70, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "026", + "Name": "Raichu", + "Classification": "Mouse Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Thunder Shock" + ], + "Weight": "30.0 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "025", + "Name": "Pikachu" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Thunder", + "Thunder Punch" + ], + "BaseAttack": 200, + "BaseDefense": 154, + "BaseStamina": 120, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "027", + "Name": "Sandshrew", + "Classification": "Mouse Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Scratch" + ], + "Weight": "12.0 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 27, + "Name": "Sandshrew candies" + }, + "Next evolution(s)": [ + { + "Number": "028", + "Name": "Sandslash" + } + ], + "Special Attack(s)": [ + "Dig", + "Rock Slide", + "Rock Tomb" + ], + "BaseAttack": 90, + "BaseDefense": 114, + "BaseStamina": 100, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "028", + "Name": "Sandslash", + "Classification": "Mouse Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Metal Claw", + "Mud Shot" + ], + "Weight": "29.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "027", + "Name": "Sandshrew" + } + ], + "Special Attack(s)": [ + "Bulldoze", + "Earthquake", + "Rock Tomb" + ], + "BaseAttack": 150, + "BaseDefense": 172, + "BaseStamina": 150, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "029", + "Name": "Nidoran F", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bite", + "Poison Sting" + ], + "Weight": "7.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 29, + "Name": "Nidoran F candies" + }, + "Next evolution(s)": [ + { + "Number": "030", + "Name": "Nidorina" + }, + { + "Number": "031", + "Name": "Nidoqueen" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Poison Fang", + "Sludge Bomb" + ], + "BaseAttack": 100, + "BaseDefense": 104, + "BaseStamina": 110, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "030", + "Name": "Nidorina", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bite", + "Poison Sting" + ], + "Weight": "20.0 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "029", + "Name": "Nidoran F" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 29, + "Name": "Nidoran F candies" + }, + "Next evolution(s)": [ + { + "Number": "031", + "Name": "Nidoqueen" + } + ], + "Special Attack(s)": [ + "Dig", + "Poison Fang", + "Sludge Bomb" + ], + "BaseAttack": 132, + "BaseDefense": 136, + "BaseStamina": 140, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "031", + "Name": "Nidoqueen", + "Classification": "Drill Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Ice", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bite", + "Poison Jab" + ], + "Weight": "60.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "029", + "Name": "Nidoran F" + }, + { + "Number": "030", + "Name": "Nidorina" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Sludge Wave", + "Stone Edge" + ], + "BaseAttack": 184, + "BaseDefense": 190, + "BaseStamina": 180, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "032", + "Name": "Nidoran M", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Peck", + "Poison Sting" + ], + "Weight": "9.0 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 32, + "Name": "Nidoran M candies" + }, + "Next evolution(s)": [ + { + "Number": "033", + "Name": "Nidorino" + }, + { + "Number": "034", + "Name": "Nidoking" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Horn Attack", + "Sludge Bomb" + ], + "BaseAttack": 110, + "BaseDefense": 94, + "BaseStamina": 92, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "033", + "Name": "Nidorino", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Poison Jab", + "Poison Sting" + ], + "Weight": "19.5 kg", + "Height": "0.9 m", + "Previous evolution(s)": [ + { + "Number": "032", + "Name": "Nidoran M" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 32, + "Name": "Nidoran M candies" + }, + "Next evolution(s)": [ + { + "Number": "034", + "Name": "Nidoking" + } + ], + "Special Attack(s)": [ + "Dig", + "Horn Attack", + "Sludge Bomb" + ], + "BaseAttack": 142, + "BaseDefense": 128, + "BaseStamina": 122, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "034", + "Name": "Nidoking", + "Classification": "Drill Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Ice", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Poison Jab" + ], + "Weight": "62.0 kg", + "Height": "1.4 m", + "Previous evolution(s)": [ + { + "Number": "032", + "Name": "Nidoran M" + }, + { + "Number": "033", + "Name": "Nidorino" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Megahorn", + "Sludge Wave" + ], + "BaseAttack": 204, + "BaseDefense": 170, + "BaseStamina": 162, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "035", + "Name": "Clefairy", + "Classification": "Fairy Pokemon", + "Type I": [ + "Fairy" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound", + "Zen Headbutt" + ], + "Weight": "7.5 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 35, + "Name": "Clefairy candies" + }, + "Next evolution(s)": [ + { + "Number": "036", + "Name": "Clefable" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Disarming Voice", + "Moonblast" + ], + "BaseAttack": 116, + "BaseDefense": 124, + "BaseStamina": 140, + "CaptureRate": 0.24, + "FleeRate": 0.1 + }, + { + "Number": "036", + "Name": "Clefable", + "Classification": "Fairy Pokemon", + "Type I": [ + "Fairy" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound", + "Zen Headbutt" + ], + "Weight": "40.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "035", + "Name": "Clefairy" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Moonblast", + "Psychic" + ], + "BaseAttack": 178, + "BaseDefense": 178, + "BaseStamina": 190, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "037", + "Name": "Vulpix", + "Classification": "Fox Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Quick Attack" + ], + "Weight": "9.9 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 37, + "Name": "Vulpix candies" + }, + "Next evolution(s)": [ + { + "Number": "038", + "Name": "Ninetales" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Flame Charge", + "Flamethrower" + ], + "BaseAttack": 106, + "BaseDefense": 118, + "BaseStamina": 76, + "CaptureRate": 0.24, + "FleeRate": 0.1 + }, + { + "Number": "038", + "Name": "Ninetales", + "Classification": "Fox Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Feint Attack" + ], + "Weight": "19.9 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "037", + "Name": "Vulpix" + } + ], + "Special Attack(s)": [ + "Fire Blast", + "Flamethrower", + "Heat Wave" + ], + "BaseAttack": 176, + "BaseDefense": 194, + "BaseStamina": 146, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "039", + "Name": "Jigglypuff", + "Classification": "Balloon Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Fairy" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Pound" + ], + "Weight": "5.5 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 39, + "Name": "Jigglypuff candies" + }, + "Next evolution(s)": [ + { + "Number": "040", + "Name": "Wigglytuff" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Disarming Voice", + "Play Rough" + ], + "BaseAttack": 98, + "BaseDefense": 54, + "BaseStamina": 230, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "040", + "Name": "Wigglytuff", + "Classification": "Balloon Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Fairy" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Pound" + ], + "Weight": "12.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "039", + "Name": "Jigglypuff" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Hyper Beam", + "Play Rough" + ], + "BaseAttack": 168, + "BaseDefense": 108, + "BaseStamina": 280, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "041", + "Name": "Zubat", + "Classification": "Bat Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Ice", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Quick Attack" + ], + "Weight": "7.5 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 41, + "Name": "Zubat candies" + }, + "Next evolution(s)": [ + { + "Number": "042", + "Name": "Golbat" + } + ], + "Special Attack(s)": [ + "Air Cutter", + "Poison Fang", + "Sludge Bomb" + ], + "BaseAttack": 88, + "BaseDefense": 90, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "042", + "Name": "Golbat", + "Classification": "Bat Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Ice", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Wing Attack" + ], + "Weight": "55.0 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "041", + "Name": "Zubat" + } + ], + "Special Attack(s)": [ + "Air Cutter", + "Ominous Wind", + "Poison Fang" + ], + "BaseAttack": 164, + "BaseDefense": 164, + "BaseStamina": 150, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "043", + "Name": "Oddish", + "Classification": "Weed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "5.4 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 43, + "Name": "Oddish candies" + }, + "Next evolution(s)": [ + { + "Number": "044", + "Name": "Gloom" + }, + { + "Number": "045", + "Name": "Vileplume" + } + ], + "Special Attack(s)": [ + "Moonblast", + "Seed Bomb", + "Sludge Bomb" + ], + "BaseAttack": 134, + "BaseDefense": 130, + "BaseStamina": 90, + "CaptureRate": 0.48, + "FleeRate": 0.15 + }, + { + "Number": "044", + "Name": "Gloom", + "Classification": "Weed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "8.6 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "043", + "Name": "Oddish" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 43, + "Name": "Oddish candies" + }, + "Next evolution(s)": [ + { + "Number": "045", + "Name": "Vileplume" + } + ], + "Special Attack(s)": [ + "Moonblast", + "Petal Blizzard", + "Sludge Bomb" + ], + "BaseAttack": 162, + "BaseDefense": 158, + "BaseStamina": 120, + "CaptureRate": 0.24, + "FleeRate": 0.07 + }, + { + "Number": "045", + "Name": "Vileplume", + "Classification": "Flower Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "18.6 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "043", + "Name": "Oddish" + }, + { + "Number": "044", + "Name": "Gloom" + } + ], + "Special Attack(s)": [ + "Moonblast", + "Petal Blizzard", + "Solar Beam" + ], + "BaseAttack": 202, + "BaseDefense": 190, + "BaseStamina": 150, + "CaptureRate": 0.12, + "FleeRate": 0.05 + }, + { + "Number": "046", + "Name": "Paras", + "Classification": "Mushroom Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Grass" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Scratch" + ], + "Weight": "5.4 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 46, + "Name": "Paras candies" + }, + "Next evolution(s)": [ + { + "Number": "047", + "Name": "Parasect" + } + ], + "Special Attack(s)": [ + "Cross Poison", + "Seed Bomb", + "X Scissor" + ], + "BaseAttack": 122, + "BaseDefense": 120, + "BaseStamina": 70, + "CaptureRate": 0.32, + "FleeRate": 0.15 + }, + { + "Number": "047", + "Name": "Parasect", + "Classification": "Mushroom Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Grass" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Fury Cutter" + ], + "Weight": "29.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "046", + "Name": "Paras" + } + ], + "Special Attack(s)": [ + "Cross Poison", + "Solar Beam", + "X Scissor" + ], + "BaseAttack": 162, + "BaseDefense": 170, + "BaseStamina": 120, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "048", + "Name": "Venonat", + "Classification": "Insect Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Confusion" + ], + "Weight": "30.0 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 48, + "Name": "Venonat candies" + }, + "Next evolution(s)": [ + { + "Number": "049", + "Name": "Venomoth" + } + ], + "Special Attack(s)": [ + "Poison Fang", + "Psybeam", + "Signal Beam" + ], + "BaseAttack": 108, + "BaseDefense": 118, + "BaseStamina": 120, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "049", + "Name": "Venomoth", + "Classification": "Poison Moth Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Confusion" + ], + "Weight": "12.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "048", + "Name": "Venonat" + } + ], + "Special Attack(s)": [ + "Bug Buzz", + "Poison Fang", + "Psychic" + ], + "BaseAttack": 172, + "BaseDefense": 154, + "BaseStamina": 140, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "050", + "Name": "Diglett", + "Classification": "Mole Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Scratch" + ], + "Weight": "0.8 kg", + "Height": "0.2 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 50, + "Name": "Diglett candies" + }, + "Next evolution(s)": [ + { + "Number": "051", + "Name": "Dugtrio" + } + ], + "Special Attack(s)": [ + "Dig", + "Mud Bomb", + "Rock Tomb" + ], + "BaseAttack": 108, + "BaseDefense": 86, + "BaseStamina": 20, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "051", + "Name": "Dugtrio", + "Classification": "Mole Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Sucker Punch" + ], + "Weight": "33.3 kg", + "Height": "0.7 m", + "Previous evolution(s)": [ + { + "Number": "050", + "Name": "Diglett" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Mud Bomb", + "Stone Edge" + ], + "BaseAttack": 148, + "BaseDefense": 140, + "BaseStamina": 70, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "052", + "Name": "Meowth", + "Classification": "Scratch Cat Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Bite", + "Scratch" + ], + "Weight": "4.2 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 52, + "Name": "Meowth candies" + }, + "Next evolution(s)": [ + { + "Number": "053", + "Name": "Persian" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Dark Pulse", + "Night Slash" + ], + "BaseAttack": 104, + "BaseDefense": 94, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "053", + "Name": "Persian", + "Classification": "Classy Cat Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Scratch" + ], + "Weight": "32.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "052", + "Name": "Meowth" + } + ], + "Special Attack(s)": [ + "Night Slash", + "Play Rough", + "Power Gem" + ], + "BaseAttack": 156, + "BaseDefense": 146, + "BaseStamina": 130, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "054", + "Name": "Psyduck", + "Classification": "Duck Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Water Gun", + "Zen Headbutt" + ], + "Weight": "19.6 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 54, + "Name": "Psyduck candies" + }, + "Next evolution(s)": [ + { + "Number": "055", + "Name": "Golduck" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Cross Chop", + "Psybeam" + ], + "BaseAttack": 132, + "BaseDefense": 112, + "BaseStamina": 100, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "055", + "Name": "Golduck", + "Classification": "Duck Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Confusion", + "Water Gun" + ], + "Weight": "76.6 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "054", + "Name": "Psyduck" + } + ], + "Special Attack(s)": [ + "Hydro Pump", + "Ice Beam", + "Psychic" + ], + "BaseAttack": 194, + "BaseDefense": 176, + "BaseStamina": 160, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "056", + "Name": "Mankey", + "Classification": "Pig Monkey Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Scratch" + ], + "Weight": "28.0 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 56, + "Name": "Mankey candies" + }, + "Next evolution(s)": [ + { + "Number": "057", + "Name": "Primeape" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Cross Chop", + "Low Sweep" + ], + "BaseAttack": 122, + "BaseDefense": 96, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "057", + "Name": "Primeape", + "Classification": "Pig Monkey Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Low Kick" + ], + "Weight": "32.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "056", + "Name": "Mankey" + } + ], + "Special Attack(s)": [ + "Cross Chop", + "Low Sweep", + "Night Slash" + ], + "BaseAttack": 178, + "BaseDefense": 150, + "BaseStamina": 130, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "058", + "Name": "Growlithe", + "Classification": "Puppy Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Ember" + ], + "Weight": "19.0 kg", + "Height": "0.7 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 58, + "Name": "Growlithe candies" + }, + "Next evolution(s)": [ + { + "Number": "059", + "Name": "Arcanine" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Flame Wheel", + "Flamethrower" + ], + "BaseAttack": 156, + "BaseDefense": 110, + "BaseStamina": 110, + "CaptureRate": 0.24, + "FleeRate": 0.1 + }, + { + "Number": "059", + "Name": "Arcanine", + "Classification": "Legendary Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Fire Fang" + ], + "Weight": "155.0 kg", + "Height": "1.9 m", + "Previous evolution(s)": [ + { + "Number": "058", + "Name": "Growlithe" + } + ], + "Special Attack(s)": [ + "Bulldoze", + "Fire Blast", + "Flamethrower" + ], + "BaseAttack": 230, + "BaseDefense": 180, + "BaseStamina": 180, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "060", + "Name": "Poliwag", + "Classification": "Tadpole Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "12.4 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 60, + "Name": "Poliwag candies" + }, + "Next evolution(s)": [ + { + "Number": "061", + "Name": "Poliwhirl" + }, + { + "Number": "062", + "Name": "Poliwrath" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Bubble Beam", + "Mud Bomb" + ], + "BaseAttack": 108, + "BaseDefense": 98, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "061", + "Name": "Poliwhirl", + "Classification": "Tadpole Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "20.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "060", + "Name": "Poliwag" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 60, + "Name": "Poliwag candies" + }, + "Next evolution(s)": [ + { + "Number": "062", + "Name": "Poliwrath" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Mud Bomb", + "Scald" + ], + "BaseAttack": 132, + "BaseDefense": 132, + "BaseStamina": 130, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "062", + "Name": "Poliwrath", + "Classification": "Tadpole Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Fighting" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "54.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "060", + "Name": "Poliwag" + }, + { + "Number": "061", + "Name": "Poliwhirl" + } + ], + "Special Attack(s)": [ + "Hydro Pump", + "Ice Punch", + "Submission" + ], + "BaseAttack": 180, + "BaseDefense": 202, + "BaseStamina": 180, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "063", + "Name": "Abra", + "Classification": "Psi Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Zen Headbutt" + ], + "Weight": "19.5 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 63, + "Name": "Abra candies" + }, + "Next evolution(s)": [ + { + "Number": "064", + "Name": "Kadabra" + }, + { + "Number": "065", + "Name": "Alakazam" + } + ], + "Special Attack(s)": [ + "Psyshock", + "Shadow Ball", + "Signal Beam" + ], + "BaseAttack": 110, + "BaseDefense": 76, + "BaseStamina": 50, + "CaptureRate": 0.4, + "FleeRate": 0.99 + }, + { + "Number": "064", + "Name": "Kadabra", + "Classification": "Psi Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Psycho Cut" + ], + "Weight": "56.5 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "063", + "Name": "Abra" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 63, + "Name": "Abra candies" + }, + "Next evolution(s)": [ + { + "Number": "065", + "Name": "Alakazam" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Psybeam", + "Shadow Ball" + ], + "BaseAttack": 150, + "BaseDefense": 112, + "BaseStamina": 80, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "065", + "Name": "Alakazam", + "Classification": "Psi Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Psycho Cut" + ], + "Weight": "48.0 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "063", + "Name": "Abra" + }, + { + "Number": "064", + "Name": "Kadabra" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Psychic", + "Shadow Ball" + ], + "BaseAttack": 186, + "BaseDefense": 152, + "BaseStamina": 110, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "066", + "Name": "Machop", + "Classification": "Superpower Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Low Kick" + ], + "Weight": "19.5 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 66, + "Name": "Machop candies" + }, + "Next evolution(s)": [ + { + "Number": "067", + "Name": "Machoke" + }, + { + "Number": "068", + "Name": "Machamp" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Cross Chop", + "Low Sweep" + ], + "BaseAttack": 118, + "BaseDefense": 96, + "BaseStamina": 140, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "067", + "Name": "Machoke", + "Classification": "Superpower Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Low Kick" + ], + "Weight": "70.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "066", + "Name": "Machop" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 66, + "Name": "Machop candies" + }, + "Next evolution(s)": [ + { + "Number": "068", + "Name": "Machamp" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Cross Chop", + "Submission" + ], + "BaseAttack": 154, + "BaseDefense": 144, + "BaseStamina": 160, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "068", + "Name": "Machamp", + "Classification": "Superpower Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Bullet Punch", + "Karate Chop" + ], + "Weight": "130.0 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "066", + "Name": "Machop" + }, + { + "Number": "067", + "Name": "Machoke" + } + ], + "Special Attack(s)": [ + "Cross Chop", + "Stone Edge", + "Submission" + ], + "BaseAttack": 198, + "BaseDefense": 180, + "BaseStamina": 180, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "069", + "Name": "Bellsprout", + "Classification": "Flower Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Vine Whip" + ], + "Weight": "4.0 kg", + "Height": "0.7 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 69, + "Name": "Bellsprout candies" + }, + "Next evolution(s)": [ + { + "Number": "070", + "Name": "Weepinbell" + }, + { + "Number": "071", + "Name": "Victreebel" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Sludge Bomb", + "Wrap" + ], + "BaseAttack": 158, + "BaseDefense": 78, + "BaseStamina": 100, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "070", + "Name": "Weepinbell", + "Classification": "Flycatcher Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "6.4 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "069", + "Name": "Bellsprout" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 69, + "Name": "Bellsprout candies" + }, + "Next evolution(s)": [ + { + "Number": "071", + "Name": "Victreebel" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Seed Bomb", + "Sludge Bomb" + ], + "BaseAttack": 190, + "BaseDefense": 110, + "BaseStamina": 130, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "071", + "Name": "Victreebel", + "Classification": "Flycatcher Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "15.5 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "069", + "Name": "Bellsprout" + }, + { + "Number": "070", + "Name": "Weepinbell" + } + ], + "Special Attack(s)": [ + "Leaf Blade", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 222, + "BaseDefense": 152, + "BaseStamina": 160, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "072", + "Name": "Tentacool", + "Classification": "Jellyfish Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Electric", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bubble", + "Poison Sting" + ], + "Weight": "45.5 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 72, + "Name": "Tentacool candies" + }, + "Next evolution(s)": [ + { + "Number": "073", + "Name": "Tentacruel" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Water Pulse", + "Wrap" + ], + "BaseAttack": 106, + "BaseDefense": 136, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "073", + "Name": "Tentacruel", + "Classification": "Jellyfish Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Electric", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Poison Jab" + ], + "Weight": "55.0 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "072", + "Name": "Tentacool" + } + ], + "Special Attack(s)": [ + "Blizzard", + "Hydro Pump", + "Sludge Wave" + ], + "BaseAttack": 170, + "BaseDefense": 196, + "BaseStamina": 160, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "074", + "Name": "Geodude", + "Classification": "Rock Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Rock Throw", + "Tackle" + ], + "Weight": "20.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 74, + "Name": "Geodude candies" + }, + "Next evolution(s)": [ + { + "Number": "075", + "Name": "Graveler" + }, + { + "Number": "076", + "Name": "Golem" + } + ], + "Special Attack(s)": [ + "Dig", + "Rock Slide", + "Rock Tomb" + ], + "BaseAttack": 106, + "BaseDefense": 118, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "075", + "Name": "Graveler", + "Classification": "Rock Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Rock Throw" + ], + "Weight": "105.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "074", + "Name": "Geodude" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 74, + "Name": "Geodude candies" + }, + "Next evolution(s)": [ + { + "Number": "076", + "Name": "Golem" + } + ], + "Special Attack(s)": [ + "Dig", + "Rock Slide", + "Stone Edge" + ], + "BaseAttack": 142, + "BaseDefense": 156, + "BaseStamina": 110, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "076", + "Name": "Golem", + "Classification": "Megaton Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Rock Throw" + ], + "Weight": "300.0 kg", + "Height": "1.4 m", + "Previous evolution(s)": [ + { + "Number": "074", + "Name": "Geodude" + }, + { + "Number": "075", + "Name": "Graveler" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Earthquake", + "Stone Edge" + ], + "BaseAttack": 176, + "BaseDefense": 198, + "BaseStamina": 160, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "077", + "Name": "Ponyta", + "Classification": "Fire Horse Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Tackle" + ], + "Weight": "30.0 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 77, + "Name": "Ponyta candies" + }, + "Next evolution(s)": [ + { + "Number": "078", + "Name": "Rapidash" + } + ], + "Special Attack(s)": [ + "Fire Blast", + "Flame Charge", + "Flame Wheel" + ], + "BaseAttack": 168, + "BaseDefense": 138, + "BaseStamina": 100, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "078", + "Name": "Rapidash", + "Classification": "Fire Horse Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Low Kick" + ], + "Weight": "95.0 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "077", + "Name": "Ponyta" + } + ], + "Special Attack(s)": [ + "Drill Run", + "Fire Blast", + "Heat Wave" + ], + "BaseAttack": 200, + "BaseDefense": 170, + "BaseStamina": 130, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "079", + "Name": "Slowpoke", + "Classification": "Dopey Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Water Gun" + ], + "Weight": "36.0 kg", + "Height": "1.2 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 79, + "Name": "Slowpoke candies" + }, + "Next evolution(s)": [ + { + "Number": "080", + "Name": "Slowbro" + } + ], + "Special Attack(s)": [ + "Psychic", + "Psyshock", + "Water Pulse" + ], + "BaseAttack": 110, + "BaseDefense": 110, + "BaseStamina": 180, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "080", + "Name": "Slowbro", + "Classification": "Hermit Crab Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Water Gun" + ], + "Weight": "78.5 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "079", + "Name": "Slowpoke" + } + ], + "Special Attack(s)": [ + "Ice Beam", + "Psychic", + "Water Pulse" + ], + "BaseAttack": 184, + "BaseDefense": 198, + "BaseStamina": 190, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "081", + "Name": "Magnemite", + "Classification": "Magnet Pokemon", + "Type I": [ + "Electric" + ], + "Type II": [ + "Steel" + ], + "Weaknesses": [ + "Fire", + "Water", + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Thunder Shock" + ], + "Weight": "6.0 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 81, + "Name": "Magnemite candies" + }, + "Next evolution(s)": [ + { + "Number": "082", + "Name": "Magneton" + } + ], + "Special Attack(s)": [ + "Discharge", + "Magnet Bomb", + "Thunderbolt" + ], + "BaseAttack": 128, + "BaseDefense": 138, + "BaseStamina": 50, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "082", + "Name": "Magneton", + "Classification": "Magnet Pokemon", + "Type I": [ + "Electric" + ], + "Type II": [ + "Steel" + ], + "Weaknesses": [ + "Fire", + "Water", + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Thunder Shock" + ], + "Weight": "60.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "081", + "Name": "Magnemite" + } + ], + "Special Attack(s)": [ + "Discharge", + "Flash Cannon", + "Magnet Bomb" + ], + "BaseAttack": 186, + "BaseDefense": 180, + "BaseStamina": 100, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "083", + "Name": "Farfetch'd", + "Classification": "Wild Duck Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Cut", + "Fury Cutter" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Leaf Blade" + ], + "Weight": "15.0 kg", + "Height": "0.8 m", + "BaseAttack": 138, + "BaseDefense": 132, + "BaseStamina": 104, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "084", + "Name": "Doduo", + "Classification": "Twin Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Peck", + "Quick Attack" + ], + "Weight": "39.2 kg", + "Height": "1.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 84, + "Name": "Doduo candies" + }, + "Next evolution(s)": [ + { + "Number": "085", + "Name": "Dodrio" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Drill Peck", + "Swift" + ], + "BaseAttack": 126, + "BaseDefense": 96, + "BaseStamina": 70, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "085", + "Name": "Dodrio", + "Classification": "Triple Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Steel Wing" + ], + "Weight": "85.2 kg", + "Height": "1.8 m", + "Previous evolution(s)": [ + { + "Number": "084", + "Name": "Doduo" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Drill Peck" + ], + "BaseAttack": 182, + "BaseDefense": 150, + "BaseStamina": 120, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "086", + "Name": "Seel", + "Classification": "Sea Lion Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Ice Shard", + "Water Gun" + ], + "Weight": "90.0 kg", + "Height": "1.1 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 86, + "Name": "Seel candies" + }, + "Next evolution(s)": [ + { + "Number": "087", + "Name": "Dewgong" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Aqua Tail", + "Icy Wind" + ], + "BaseAttack": 104, + "BaseDefense": 138, + "BaseStamina": 130, + "CaptureRate": 0.4, + "FleeRate": 0.09 + }, + { + "Number": "087", + "Name": "Dewgong", + "Classification": "Sea Lion Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Ice" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Rock" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Ice Shard" + ], + "Weight": "120.0 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "086", + "Name": "Seel" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Blizzard", + "Icy Wind" + ], + "BaseAttack": 156, + "BaseDefense": 192, + "BaseStamina": 180, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "088", + "Name": "Grimer", + "Classification": "Sludge Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Mud Slap" + ], + "Weight": "30.0 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 88, + "Name": "Grimer candies" + }, + "Next evolution(s)": [ + { + "Number": "089", + "Name": "Muk" + } + ], + "Special Attack(s)": [ + "Mud Bomb", + "Sludge", + "Sludge Bomb" + ], + "BaseAttack": 124, + "BaseDefense": 110, + "BaseStamina": 160, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "089", + "Name": "Muk", + "Classification": "Sludge Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Poison Jab" + ], + "Weight": "30.0 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "088", + "Name": "Grimer" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Gunk Shot", + "Sludge Wave" + ], + "BaseAttack": 180, + "BaseDefense": 188, + "BaseStamina": 210, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "090", + "Name": "Shellder", + "Classification": "Bivalve Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Ice Shard", + "Tackle" + ], + "Weight": "4.0 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 90, + "Name": "Shellder candies" + }, + "Next evolution(s)": [ + { + "Number": "091", + "Name": "Cloyster" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Icy Wind", + "Water Pulse" + ], + "BaseAttack": 120, + "BaseDefense": 112, + "BaseStamina": 60, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "091", + "Name": "Cloyster", + "Classification": "Bivalve Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Ice" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Rock" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Ice Shard" + ], + "Weight": "132.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "090", + "Name": "Shellder" + } + ], + "Special Attack(s)": [ + "Blizzard", + "Hydro Pump", + "Icy Wind" + ], + "BaseAttack": 196, + "BaseDefense": 196, + "BaseStamina": 100, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "092", + "Name": "Gastly", + "Classification": "Gas Pokemon", + "Type I": [ + "Ghost" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Lick", + "Sucker Punch" + ], + "Weight": "0.1 kg", + "Height": "1.3 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 92, + "Name": "Gastly candies" + }, + "Next evolution(s)": [ + { + "Number": "093", + "Name": "Haunter" + }, + { + "Number": "094", + "Name": "Gengar" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Ominous Wind", + "Sludge Bomb" + ], + "BaseAttack": 136, + "BaseDefense": 82, + "BaseStamina": 60, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "093", + "Name": "Haunter", + "Classification": "Gas Pokemon", + "Type I": [ + "Ghost" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Lick", + "Shadow Claw" + ], + "Weight": "0.1 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "092", + "Name": "Gastly" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 92, + "Name": "Gastly candies" + }, + "Next evolution(s)": [ + { + "Number": "094", + "Name": "Gengar" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Shadow Ball", + "Sludge Bomb" + ], + "BaseAttack": 172, + "BaseDefense": 118, + "BaseStamina": 90, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "094", + "Name": "Gengar", + "Classification": "Shadow Pokemon", + "Type I": [ + "Ghost" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Shadow Claw", + "Sucker Punch" + ], + "Weight": "40.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "092", + "Name": "Gastly" + }, + { + "Number": "093", + "Name": "Haunter" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Shadow Ball", + "Sludge Wave" + ], + "BaseAttack": 204, + "BaseDefense": 156, + "BaseStamina": 120, + "CaptureRate": 0.08, + "FleeRate": 0.05 + }, + { + "Number": "095", + "Name": "Onix", + "Classification": "Rock Snake Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Rock Throw", + "Tackle" + ], + "Weight": "210.0 kg", + "Height": "8.8 m", + "Special Attack(s)": [ + "Iron Head", + "Rock Slide", + "Stone Edge" + ], + "BaseAttack": 90, + "BaseDefense": 186, + "BaseStamina": 70, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "096", + "Name": "Drowzee", + "Classification": "Hypnosis Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Pound" + ], + "Weight": "32.4 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 96, + "Name": "Drowzee candies" + }, + "Next evolution(s)": [ + { + "Number": "097", + "Name": "Hypno" + } + ], + "Special Attack(s)": [ + "Psybeam", + "Psychic", + "Psyshock" + ], + "BaseAttack": 104, + "BaseDefense": 140, + "BaseStamina": 120, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "097", + "Name": "Hypno", + "Classification": "Hypnosis Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Zen Headbutt" + ], + "Weight": "75.6 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "096", + "Name": "Drowzee" + } + ], + "Special Attack(s)": [ + "Psychic", + "Psyshock", + "Shadow Ball" + ], + "BaseAttack": 162, + "BaseDefense": 196, + "BaseStamina": 170, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "098", + "Name": "Krabby", + "Classification": "River Crab Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "6.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 98, + "Name": "Krabby candies" + }, + "Next evolution(s)": [ + { + "Number": "099", + "Name": "Kingler" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Vice Grip", + "Water Pulse" + ], + "BaseAttack": 116, + "BaseDefense": 110, + "BaseStamina": 60, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "099", + "Name": "Kingler", + "Classification": "Pincer Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Metal Claw", + "Mud Shot" + ], + "Weight": "60.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "098", + "Name": "Krabby" + } + ], + "Special Attack(s)": [ + "Vice Grip", + "Water Pulse", + "X Scissor" + ], + "BaseAttack": 178, + "BaseDefense": 168, + "BaseStamina": 110, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "100", + "Name": "Voltorb", + "Classification": "Ball Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Tackle" + ], + "Weight": "10.4 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 100, + "Name": "Voltorb candies" + }, + "Next evolution(s)": [ + { + "Number": "101", + "Name": "Electrode" + } + ], + "Special Attack(s)": [ + "Discharge", + "Signal Beam", + "Thunderbolt" + ], + "BaseAttack": 102, + "BaseDefense": 124, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "101", + "Name": "Electrode", + "Classification": "Ball Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Tackle" + ], + "Weight": "66.6 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "100", + "Name": "Voltorb" + } + ], + "Special Attack(s)": [ + "Discharge", + "Hyper Beam", + "Thunderbolt" + ], + "BaseAttack": 150, + "BaseDefense": 174, + "BaseStamina": 120, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "102", + "Name": "Exeggcute", + "Classification": "Egg Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion" + ], + "Weight": "2.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 102, + "Name": "Exeggcute candies" + }, + "Next evolution(s)": [ + { + "Number": "103", + "Name": "Exeggutor" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Psychic", + "Seed Bomb" + ], + "BaseAttack": 110, + "BaseDefense": 132, + "BaseStamina": 120, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "103", + "Name": "Exeggutor", + "Classification": "Coconut Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Zen Headbutt" + ], + "Weight": "120.0 kg", + "Height": "2.0 m", + "Previous evolution(s)": [ + { + "Number": "102", + "Name": "Exeggcute" + } + ], + "Special Attack(s)": [ + "Psychic", + "Seed Bomb", + "Solar Beam" + ], + "BaseAttack": 232, + "BaseDefense": 164, + "BaseStamina": 190, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "104", + "Name": "Cubone", + "Classification": "Lonely Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "6.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 104, + "Name": "Cubone candies" + }, + "Next evolution(s)": [ + { + "Number": "105", + "Name": "Marowak" + } + ], + "Special Attack(s)": [ + "Bone Club", + "Bulldoze", + "Dig" + ], + "BaseAttack": 102, + "BaseDefense": 150, + "BaseStamina": 100, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "105", + "Name": "Marowak", + "Classification": "Bone Keeper Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "45.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "104", + "Name": "Cubone" + } + ], + "Special Attack(s)": [ + "Bone Club", + "Dig", + "Earthquake" + ], + "BaseAttack": 140, + "BaseDefense": 202, + "BaseStamina": 120, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "106", + "Name": "Hitmonlee", + "Classification": "Kicking Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Low Kick", + "Rock Smash" + ], + "Weight": "49.8 kg", + "Height": "1.5 m", + "Special Attack(s)": [ + "Low Sweep", + "Stomp", + "Stone Edge" + ], + "BaseAttack": 148, + "BaseDefense": 172, + "BaseStamina": 100, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "107", + "Name": "Hitmonchan", + "Classification": "Punching Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Bullet Punch", + "Rock Smash" + ], + "Weight": "50.2 kg", + "Height": "1.4 m", + "Special Attack(s)": [ + "Brick Break", + "Fire Punch", + "Ice Punch", + "Thunder Punch" + ], + "BaseAttack": 138, + "BaseDefense": 204, + "BaseStamina": 100, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "108", + "Name": "Lickitung", + "Classification": "Licking Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Lick", + "Zen Headbutt" + ], + "Weight": "65.5 kg", + "Height": "1.2 m", + "Special Attack(s)": [ + "Hyper Beam", + "Power Whip", + "Stomp" + ], + "BaseAttack": 126, + "BaseDefense": 160, + "BaseStamina": 180, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "109", + "Name": "Koffing", + "Classification": "Poison Gas Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Tackle" + ], + "Weight": "1.0 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 109, + "Name": "Koffing candies" + }, + "Next evolution(s)": [ + { + "Number": "110", + "Name": "Weezing" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Sludge", + "Sludge Bomb" + ], + "BaseAttack": 136, + "BaseDefense": 142, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "110", + "Name": "Weezing", + "Classification": "Poison Gas Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Tackle" + ], + "Weight": "9.5 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "109", + "Name": "Koffing" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Shadow Ball", + "Sludge Bomb" + ], + "BaseAttack": 190, + "BaseDefense": 198, + "BaseStamina": 130, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "111", + "Name": "Rhyhorn", + "Classification": "Spikes Pokemon", + "Type I": [ + "Ground" + ], + "Type II": [ + "Rock" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "115.0 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 111, + "Name": "Rhyhorn candies" + }, + "Next evolution(s)": [ + { + "Number": "112", + "Name": "Rhydon" + } + ], + "Special Attack(s)": [ + "Bulldoze", + "Horn Attack", + "Stomp" + ], + "BaseAttack": 110, + "BaseDefense": 116, + "BaseStamina": 160, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "112", + "Name": "Rhydon", + "Classification": "Drill Pokemon", + "Type I": [ + "Ground" + ], + "Type II": [ + "Rock" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "120.0 kg", + "Height": "1.9 m", + "Previous evolution(s)": [ + { + "Number": "111", + "Name": "Rhyhorn" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Megahorn", + "Stone Edge" + ], + "BaseAttack": 166, + "BaseDefense": 160, + "BaseStamina": 210, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "113", + "Name": "Chansey", + "Classification": "Egg Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound", + "Zen Headbutt" + ], + "Weight": "34.6 kg", + "Height": "1.1 m", + "Special Attack(s)": [ + "Dazzling Gleam", + "Psybeam", + "Psychic" + ], + "BaseAttack": 40, + "BaseDefense": 60, + "BaseStamina": 500, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "114", + "Name": "Tangela", + "Classification": "Vine Pokemon", + "Type I": [ + "Grass" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug" + ], + "Fast Attack(s)": [ + "Vine Whip" + ], + "Weight": "35.0 kg", + "Height": "1.0 m", + "Special Attack(s)": [ + "Power Whip", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 164, + "BaseDefense": 152, + "BaseStamina": 130, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "115", + "Name": "Kangaskhan", + "Classification": "Parent Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Low Kick", + "Mud Slap" + ], + "Weight": "80.0 kg", + "Height": "2.2 m", + "Special Attack(s)": [ + "Brick Break", + "Earthquake", + "Stomp" + ], + "BaseAttack": 142, + "BaseDefense": 178, + "BaseStamina": 210, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "116", + "Name": "Horsea", + "Classification": "Dragon Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Water Gun" + ], + "Weight": "8.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 116, + "Name": "Horsea candies" + }, + "Next evolution(s)": [ + { + "Number": "117", + "Name": "Seadra" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Dragon Pulse", + "Flash Cannon" + ], + "BaseAttack": 122, + "BaseDefense": 100, + "BaseStamina": 60, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "117", + "Name": "Seadra", + "Classification": "Dragon Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Dragon Breath", + "Water Gun" + ], + "Weight": "25.0 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "116", + "Name": "Horsea" + } + ], + "Special Attack(s)": [ + "Blizzard", + "Dragon Pulse", + "Hydro Pump" + ], + "BaseAttack": 176, + "BaseDefense": 150, + "BaseStamina": 110, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "118", + "Name": "Goldeen", + "Classification": "Goldfish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Peck" + ], + "Weight": "15.0 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 118, + "Name": "Goldeen candies" + }, + "Next evolution(s)": [ + { + "Number": "119", + "Name": "Seaking" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Horn Attack", + "Water Pulse" + ], + "BaseAttack": 112, + "BaseDefense": 126, + "BaseStamina": 90, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "119", + "Name": "Seaking", + "Classification": "Goldfish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Peck", + "Poison Jab" + ], + "Weight": "39.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "118", + "Name": "Goldeen" + } + ], + "Special Attack(s)": [ + "Drill Run", + "Icy Wind", + "Megahorn" + ], + "BaseAttack": 172, + "BaseDefense": 160, + "BaseStamina": 160, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "120", + "Name": "Staryu", + "Classification": "Starshape Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Water Gun" + ], + "Weight": "34.5 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 120, + "Name": "Staryu candies" + }, + "Next evolution(s)": [ + { + "Number": "121", + "Name": "Starmie" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Power Gem", + "Swift" + ], + "BaseAttack": 130, + "BaseDefense": 128, + "BaseStamina": 60, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "121", + "Name": "Starmie", + "Classification": "Mysterious Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Water Gun" + ], + "Weight": "80.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "120", + "Name": "Staryu" + } + ], + "Special Attack(s)": [ + "Hydro Pump", + "Power Gem", + "Psybeam" + ], + "BaseAttack": 194, + "BaseDefense": 192, + "BaseStamina": 120, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "122", + "Name": "Mr. Mime", + "Classification": "Barrier Pokemon", + "Type I": [ + "Psychic" + ], + "Type II": [ + "Fairy" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Zen Headbutt" + ], + "Weight": "54.5 kg", + "Height": "1.3 m", + "Special Attack(s)": [ + "Psybeam", + "Psychic", + "Shadow Ball" + ], + "BaseAttack": 154, + "BaseDefense": 196, + "BaseStamina": 80, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "123", + "Name": "Scyther", + "Classification": "Mantis Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Fire", + "Electric", + "Ice", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Steel Wing" + ], + "Weight": "56.0 kg", + "Height": "1.5 m", + "Special Attack(s)": [ + "Bug Buzz", + "Night Slash", + "X Scissor" + ], + "BaseAttack": 176, + "BaseDefense": 180, + "BaseStamina": 140, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "124", + "Name": "Jynx", + "Classification": "Humanshape Pokemon", + "Type I": [ + "Ice" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Fire", + "Bug", + "Rock", + "Ghost", + "Dark", + "Steel" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Pound" + ], + "Weight": "40.6 kg", + "Height": "1.4 m", + "Special Attack(s)": [ + "Draining Kiss", + "Ice Punch", + "Psyshock" + ], + "BaseAttack": 172, + "BaseDefense": 134, + "BaseStamina": 130, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "125", + "Name": "Electabuzz", + "Classification": "Electric Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Low Kick", + "Thunder Shock" + ], + "Weight": "30.0 kg", + "Height": "1.1 m", + "Special Attack(s)": [ + "Thunder", + "Thunder Punch", + "Thunderbolt" + ], + "BaseAttack": 198, + "BaseDefense": 160, + "BaseStamina": 130, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "126", + "Name": "Magmar", + "Classification": "Spitfire Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Karate Chop" + ], + "Weight": "44.5 kg", + "Height": "1.3 m", + "Special Attack(s)": [ + "Fire Blast", + "Fire Punch", + "Flamethrower" + ], + "BaseAttack": 214, + "BaseDefense": 158, + "BaseStamina": 130, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "127", + "Name": "Pinsir", + "Classification": "Stagbeetle Pokemon", + "Type I": [ + "Bug" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Rock Smash" + ], + "Weight": "55.0 kg", + "Height": "1.5 m", + "Special Attack(s)": [ + "Submission", + "Vice Grip", + "X Scissor" + ], + "BaseAttack": 184, + "BaseDefense": 186, + "BaseStamina": 130, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "128", + "Name": "Tauros", + "Classification": "Wild Bull Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Tackle", + "Zen Headbutt" + ], + "Weight": "88.4 kg", + "Height": "1.4 m", + "Special Attack(s)": [ + "Earthquake", + "Horn Attack", + "Iron Head" + ], + "BaseAttack": 148, + "BaseDefense": 184, + "BaseStamina": 150, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "129", + "Name": "Magikarp", + "Classification": "Fish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Splash" + ], + "Weight": "10.0 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 400, + "Family": 129, + "Name": "Magikarp candies" + }, + "Next evolution(s)": [ + { + "Number": "130", + "Name": "Gyarados" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 42, + "BaseDefense": 84, + "BaseStamina": 40, + "CaptureRate": 0.56, + "FleeRate": 0.15 + }, + { + "Number": "130", + "Name": "Gyarados", + "Classification": "Atrocious Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Dragon Breath" + ], + "Weight": "235.0 kg", + "Height": "6.5 m", + "Previous evolution(s)": [ + { + "Number": "129", + "Name": "Magikarp" + } + ], + "Special Attack(s)": [ + "Dragon Pulse", + "Hydro Pump", + "Twister" + ], + "BaseAttack": 192, + "BaseDefense": 196, + "BaseStamina": 190, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "131", + "Name": "Lapras", + "Classification": "Transport Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Ice" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Rock" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Ice Shard" + ], + "Weight": "220.0 kg", + "Height": "2.5 m", + "Special Attack(s)": [ + "Blizzard", + "Dragon Pulse", + "Ice Beam" + ], + "BaseAttack": 186, + "BaseDefense": 190, + "BaseStamina": 260, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "132", + "Name": "Ditto", + "Classification": "Transform Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound" + ], + "Special Attack(s)": [ + "Struggle" + ], + "Weight": "4.0 kg", + "Height": "0.3 m", + "BaseAttack": 110, + "BaseDefense": 110, + "BaseStamina": 96, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "133", + "Name": "Eevee", + "Classification": "Evolution Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Weight": "6.5 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 133, + "Name": "Eevee candies" + }, + "Next evolution(s)": [ + { + "Number": "134", + "Name": "Vaporeon" + }, + { + "Number": "135", + "Name": "Jolteon" + }, + { + "Number": "136", + "Name": "Flareon" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Dig", + "Swift" + ], + "BaseAttack": 114, + "BaseDefense": 128, + "BaseStamina": 110, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "134", + "Name": "Vaporeon", + "Classification": "Bubble Jet Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Water Gun" + ], + "Weight": "29.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "133", + "Name": "Eevee" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Hydro Pump", + "Water Pulse" + ], + "BaseAttack": 186, + "BaseDefense": 168, + "BaseStamina": 260, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "135", + "Name": "Jolteon", + "Classification": "Lightning Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Thunder Shock" + ], + "Weight": "24.5 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "133", + "Name": "Eevee" + } + ], + "Special Attack(s)": [ + "Discharge", + "Thunder", + "Thunderbolt" + ], + "BaseAttack": 192, + "BaseDefense": 174, + "BaseStamina": 130, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "136", + "Name": "Flareon", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember" + ], + "Weight": "25.0 kg", + "Height": "0.9 m", + "Previous evolution(s)": [ + { + "Number": "133", + "Name": "Eevee" + } + ], + "Special Attack(s)": [ + "Fire Blast", + "Flamethrower", + "Heat Wave" + ], + "BaseAttack": 238, + "BaseDefense": 178, + "BaseStamina": 130, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "137", + "Name": "Porygon", + "Classification": "Virtual Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Weight": "36.5 kg", + "Height": "0.8 m", + "Special Attack(s)": [ + "Discharge", + "Psybeam", + "Signal Beam" + ], + "BaseAttack": 156, + "BaseDefense": 158, + "BaseStamina": 130, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "138", + "Name": "Omanyte", + "Classification": "Spiral Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Water Gun" + ], + "Weight": "7.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 138, + "Name": "Omanyte candies" + }, + "Next evolution(s)": [ + { + "Number": "139", + "Name": "Omastar" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Brine", + "Rock Tomb" + ], + "BaseAttack": 132, + "BaseDefense": 160, + "BaseStamina": 70, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "139", + "Name": "Omastar", + "Classification": "Spiral Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Rock Throw", + "Water Gun" + ], + "Weight": "35.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "138", + "Name": "Omanyte" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Hydro Pump", + "Rock Slide" + ], + "BaseAttack": 180, + "BaseDefense": 202, + "BaseStamina": 140, + "CaptureRate": 0.12, + "FleeRate": 0.05 + }, + { + "Number": "140", + "Name": "Kabuto", + "Classification": "Shellfish Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Scratch" + ], + "Weight": "11.5 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 140, + "Name": "Kabuto candies" + }, + "Next evolution(s)": [ + { + "Number": "141", + "Name": "Kabutops" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Aqua Jet", + "Rock Tomb" + ], + "BaseAttack": 148, + "BaseDefense": 142, + "BaseStamina": 60, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "141", + "Name": "Kabutops", + "Classification": "Shellfish Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Mud Shot" + ], + "Weight": "40.5 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "140", + "Name": "Kabuto" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Stone Edge", + "Water Pulse" + ], + "BaseAttack": 190, + "BaseDefense": 190, + "BaseStamina": 120, + "CaptureRate": 0.12, + "FleeRate": 0.05 + }, + { + "Number": "142", + "Name": "Aerodactyl", + "Classification": "Fossil Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Water", + "Electric", + "Ice", + "Rock", + "Steel" + ], + "Fast Attack(s)": [ + "Bite", + "Steel Wing" + ], + "Weight": "59.0 kg", + "Height": "1.8 m", + "Special Attack(s)": [ + "Ancient Power", + "Hyper Beam", + "Iron Head" + ], + "BaseAttack": 182, + "BaseDefense": 162, + "BaseStamina": 160, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "143", + "Name": "Snorlax", + "Classification": "Sleeping Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Lick", + "Zen Headbutt" + ], + "Weight": "460.0 kg", + "Height": "2.1 m", + "Special Attack(s)": [ + "Body Slam", + "Earthquake", + "Hyper Beam" + ], + "BaseAttack": 180, + "BaseDefense": 180, + "BaseStamina": 320, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "144", + "Name": "Articuno", + "Classification": "Freeze Pokemon", + "Type I": [ + "Ice" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Fire", + "Electric", + "Rock", + "Steel" + ], + "Fast Attack(s)": [ + "Frost Breath" + ], + "Special Attack(s)": [ + "Blizzard", + "Ice Beam", + "Icy Wind" + ], + "Weight": "55.4 kg", + "Height": "1.7 m", + "BaseAttack": 198, + "BaseDefense": 242, + "BaseStamina": 180, + "CaptureRate": 0, + "FleeRate": 0.1 + }, + { + "Number": "145", + "Name": "Zapdos", + "Classification": "Electric Pokemon", + "Type I": [ + "Electric" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Ice", + "Rock" + ], + "Fast Attack(s)": [ + "Thunder Shock" + ], + "Special Attack(s)": [ + "Discharge", + "Thunder", + "Thunderbolt" + ], + "Weight": "52.6 kg", + "Height": "1.6 m", + "BaseAttack": 232, + "BaseDefense": 194, + "BaseStamina": 180, + "CaptureRate": 0, + "FleeRate": 0.1 + }, + { + "Number": "146", + "Name": "Moltres", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Water", + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Ember" + ], + "Special Attack(s)": [ + "Fire Blast", + "Flamethrower", + "Heat Wave" + ], + "Weight": "60.0 kg", + "Height": "2.0 m", + "BaseAttack": 242, + "BaseDefense": 194, + "BaseStamina": 180, + "CaptureRate": 0, + "FleeRate": 0.1 + }, + { + "Number": "147", + "Name": "Dratini", + "Classification": "Dragon Pokemon", + "Type I": [ + "Dragon" + ], + "Weaknesses": [ + "Ice", + "Dragon", + "Fairy" + ], + "Fast Attack(s)": [ + "Dragon Breath" + ], + "Weight": "3.3 kg", + "Height": "1.8 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 147, + "Name": "Dratini candies" + }, + "Next evolution(s)": [ + { + "Number": "148", + "Name": "Dragonair" + }, + { + "Number": "149", + "Name": "Dragonite" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Twister", + "Wrap" + ], + "BaseAttack": 128, + "BaseDefense": 110, + "BaseStamina": 82, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "148", + "Name": "Dragonair", + "Classification": "Dragon Pokemon", + "Type I": [ + "Dragon" + ], + "Weaknesses": [ + "Ice", + "Dragon", + "Fairy" + ], + "Fast Attack(s)": [ + "Dragon Breath" + ], + "Weight": "16.5 kg", + "Height": "4.0 m", + "Previous evolution(s)": [ + { + "Number": "147", + "Name": "Dratini" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 147, + "Name": "Dratini candies" + }, + "Next evolution(s)": [ + { + "Number": "149", + "Name": "Dragonite" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Dragon Pulse", + "Wrap" + ], + "BaseAttack": 170, + "BaseDefense": 152, + "BaseStamina": 122, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "149", + "Name": "Dragonite", + "Classification": "Dragon Pokemon", + "Type I": [ + "Dragon" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Ice", + "Rock", + "Dragon", + "Fairy" + ], + "Fast Attack(s)": [ + "Dragon Breath", + "Steel Wing" + ], + "Weight": "210.0 kg", + "Height": "2.2 m", + "Previous evolution(s)": [ + { + "Number": "147", + "Name": "Dratini" + }, + { + "Number": "148", + "Name": "Dragonair" + } + ], + "Special Attack(s)": [ + "Dragon Claw", + "Dragon Pulse", + "Hyper Beam" + ], + "BaseAttack": 250, + "BaseDefense": 212, + "BaseStamina": 182, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "150", + "Name": "Mewtwo", + "Classification": "Genetic Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Psycho Cut" + ], + "Special Attack(s)": [ + "Hyper Beam", + "Psychic", + "Shadow Ball" + ], + "Weight": "122.0 kg", + "Height": "2.0 m", + "BaseAttack": 284, + "BaseDefense": 202, + "BaseStamina": 212, + "CaptureRate": 0, + "FleeRate": 0.1 + }, + { + "Number": "151", + "Name": "Mew", + "Classification": "New Species Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Pound" + ], + "Special Attack(s)": [ + "Dragon Pulse", + "Earthquake", + "Fire Blast", + "Hurricane", + "Hyper Beam", + "Moonblast", + "Psychic", + "Solar Beam", + "Thunder" + ], + "Weight": "4.0 kg", + "Height": "0.4 m", + "BaseAttack": 220, + "BaseDefense": 220, + "BaseStamina": 200, + "CaptureRate": 0, + "FleeRate": 0.1 + } +] diff --git a/docs/auto_restart.md b/docs/auto_restart.md new file mode 100644 index 0000000000..e85752018c --- /dev/null +++ b/docs/auto_restart.md @@ -0,0 +1,73 @@ +This page is for a workaround to restart your bot(s). +_(Restarting is superior over reconnecting in case of stability for crashes)_ + +# MAC OS +1. Open your terminal +Just open it and you finished step 1 + +2. Create a new apple script +Heres an example to start and restart bots (in separate folders) adjust it for your needs. (paths, start commands, restart timer, ...) + +You can create a start file (if you are lazy :P) and a restart file or just one for both needs + +Start script: + + tell application "Terminal" + activate + do script "cd desktop" in selected tab of the front window #edit your path + do script "cd bots" in selected tab of the front window #edit your path + do script "cd bot1" in selected tab of the front window #edit your path + do script "python pokecli.py" in selected tab of the front window #start with your parameters + + #add more bots + delay 10 + tell application "System Events" + keystroke "t" using {command down} #open a new tab for next bot + end tell + delay 5 + do script "cd .." in selected tab of the front window + do script "cd bot2" in selected tab of the front window + do script "python pokecli.py" in selected tab of the front window + #copy this part for the amount you need + end tell + +restart: + + repeat + + delay 1200 #timer in seconds + tell application "Terminal" + activate + + tell application "System Events" + keystroke "c" using {control down} #close the bot + end tell + delay 3 + do script "clear" in selected tab of the front window #not needed just for nice view + delay 3 + do script "python pokecli.py" in selected tab of the front window #restart with parameters + + #copy for the amount of bots + delay 10 + tell application "System Events" + keystroke "ö" using {command down} #going to the previous tab + end tell + delay 3 + + tell application "System Events" + keystroke "c" using {control down} + end tell + delay 3 + do script "clear" in selected tab of the front window + delay 3 + do script "python pokecli.py" in selected tab of the front window + + #copy for the amount of bots + tell application "System Events" + keystroke "ä" using {command down} #moving the the last tab + end tell + delay 3 + + end tell + + end repeat diff --git a/docs/configuration_files.md b/docs/configuration_files.md new file mode 100644 index 0000000000..a881740b9a --- /dev/null +++ b/docs/configuration_files.md @@ -0,0 +1,271 @@ +## Usage (up-to-date) + 1. copy `config.json.example` to `config.json`. + 2. Edit `config.json` and replace `auth_service`, `username`, `password`, `location` and `gmapkey` with your parameters (other keys are optional, check `Advance Configuration` below) + 3. Simply launch the script with : `./run.sh` or `./pokecli.py` or `python pokecli.py -cf ./configs/config.json` if you want to specify a config file + +## Advanced Configuration +| Parameter | Default | Description | +|------------------|-------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `tasks` | [] | The behaviors you want the bot to do. Read [how to configure tasks](#configuring-tasks). +| `max_steps` | 5 | The steps around your initial location (DEFAULT 5 mean 25 cells around your location) that will be explored +| `forts.avoid_circles` | False | Set whether the bot should avoid circles | +| `forts.max_circle_size` | 10 | How many forts to keep in ignore list | +| `walk` | 4.16 | Set the walking speed in kilometers per hour. (14 km/h is the maximum speed for egg hatching) | +| `action_wait_min` | 1 | Set the minimum time setting for anti-ban time randomizer +| `action_wait_max` | 4 | Set the maximum time setting for anti-ban time randomizer +| `debug` | false | Let the default value here except if you are developer | +| `test` | false | Let the default value here except if you are developer | | +| `location_cache` | true | Bot will start at last known location if you do not have location set in the config | +| `distance_unit` | km | Set the unit to display distance in (km for kilometers, mi for miles, ft for feet) | +| `evolve_cp_min` | 300 | Min. CP for evolve_all function + +## Configuring Tasks +The behaviors of the bot are configured via the `tasks` key in the `config.json`. This enables you to list what you want the bot to do and change the priority of those tasks by reordering them in the list. This list of tasks is run repeatedly and in order. For more information on why we are moving config to this format, check out the [original proposal](https://github.com/PokemonGoF/PokemonGo-Bot/issues/142). + +### Task Options: +* CatchLuredPokemon +* CatchVisiblePokemon +* EvolvePokemon + * `evolve_all`: Default `NONE` | Set to `"all"` to evolve Pokémon if possible when the bot starts. Can also be set to individual Pokémon as well as multiple separated by a comma. e.g "Pidgey,Rattata,Weedle,Zubat" + * `evolve_speed`: Default `20` + * `use_lucky_egg`: Default: `False` +* FollowPath + * `path_mode`: Default `loop` | Set the mode for the path navigator (loop or linear). + * `path_file`: Default `NONE` | Set the file containing the waypoints for the path navigator. +* FollowSpiral +* HandleSoftBan +* IncubateEggs + * `longer_eggs_first`: Default `True` +* MoveToFort +* [MoveToMapPokemon](#sniping-movetolocation) +* NicknamePokemon + * `nickname_template`: Default `""` | See the [Pokemon Nicknaming](#pokemon-nicknaming) section for more details +* RecycleItems + * `item_filter`: Pass a list of unwanted [items (using their JSON codes)](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Item-ID's) to recycle when collected at a Pokestop +* SpinFort +* TransferPokemon + +### Example configuration: +The following configuration tells the bot to transfer all the Pokemon that match the transfer configuration rules, then recycle the items that match its configuration, then catch the pokemon that it can, so on, so forth. Note the last two tasks, MoveToFort and FollowSpiral. When a task is still in progress, it won't run the next things in the list. So it will move towards the fort, on each step running through the list of tasks again. Only when it arrives at the fort and there are no other stops available for it to move towards will it continue to the next step and follow the spiral. + +``` +{ + // ... + "tasks": [ + { + "type": "TransferPokemon" + }, + { + "type": "RecycleItems" + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort" + }, + { + "type": "MoveToFort" + }, + { + "type": "FollowSpiral" + } + ] + // ... +} +``` + +### Specifying configuration for tasks +If you want to configure a given task, you can pass values like this: + +``` +{ + // ... + "tasks": [ + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + } + ] + // ... +} +``` + +### An example task configuration if you only wanted to collect items from forts: +``` +{ + // ... + "tasks": [ + { + "type": "RecycleItems" + }, + { + "type": "SpinFortWorker" + }, + { + "type": "MoveToFortWorker" + } + ], + // ... +} +``` + +## Catch Configuration +Default configuration will capture all Pokémon. + +```"any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}``` + +You can override the global configuration with Pokémon-specific options, such as: + +```"Pidgey": {"catch_above_cp": 0, "catch_above_iv": 0.8", "logic": "and"}``` to only capture Pidgey with a good roll. + +Additionally, you can specify always_capture and never_capture flags. + +For example: ```"Pidgey": {"never_capture": true}``` will stop catching Pidgey entirely. + +## Release Configuration + +### Common configuration + +Default configuration will not release any Pokémon. + +```"release": {"any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}}``` + +You can override the global configuration with Pokémon-specific options, such as: + +```"release": {"Pidgey": {"release_below_cp": 0, "release_below_iv": 0.8, "logic": "or"}}``` to only release Pidgey with bad rolls. + +Additionally, you can specify always_release and never_release flags. For example: + +```"release": {"Pidgey": {"always_release": true}}``` will release all Pidgey caught. + +### Keep the strongest pokemon configuration (dev branch) + +You can set ```"release": {"Pidgey": {"keep_best_cp": 1}}``` or ```"release": {"any": {"keep_best_iv": 1}}```. + +In that case after each capture bot will check that do you have a new Pokémon or not. + +If you don't have it, it will keep it (no matter was it strong or weak Pokémon). + +If you already have it, it will keep a stronger version and will transfer the a weaker one. + +```"release": {"any": {"keep_best_cp": 2}}```, ```"release": {"any": {"keep_best_cp": 10}}``` - can be any number. + +## Evolve All Configuration + +By setting the `evolve_all` attribute in config.json, you can instruct the bot to automatically +evolve specified Pokémon on startup. This is especially useful for batch-evolving after popping up +a lucky egg (currently this needs to be done manually). + +The evolve all mechanism evolves only higher IV/CP Pokémon. It works by sorting the high CP Pokémon (default: 300 CP or higher) +based on their IV values. After evolving all high CP Pokémon, the mechanism will move on to evolving lower CP Pokémon +only based on their CP (if it can). +It will also automatically transfer the evolved Pokémon based on the release configuration. + +Examples on how to use (set in config.json): + +1. "evolve_all": "all" + Will evolve ALL Pokémon. + +2. "evolve_all": "Pidgey,Weedle" + Will only evolve Pidgey and Weedle. + +3. Not setting evolve_all or having any other string would not evolve any Pokémon on startup. + +If you wish to change the default threshold of 300 CP, simply add the following to the config file: + +``` +"evolve_cp_min": +``` + +## Path Navigator Configuration + +Setting the `navigator.type` setting to `path` allows you to specify waypoints which the bot will follow. The waypoints can be loaded from a GPX or JSON file. By default the bot will walk along all specified waypoints and then move directly to the first waypoint again. When setting `navigator.path_mode` to `linear`, the bot will turn around at the last waypoint and along the given waypoints in reverse order. + +An example for a JSON file can be found in `configs/path.example.json`. GPX files can be exported from many online tools, such as gpsies.com.The bot loads the first segment of the first track. + +## Pokemon Nicknaming + +A `nickname_template` can be specified for the `NicknamePokemon` task to allow a nickname template to be applied to all pokemon in the user's inventory. For example, a user wanting all their pokemon to have their IV values as their nickname could use a template `{iv_ads}`, which will cause their pokemon to be named something like `13/7/12` (depending on the pokemon's actual IVs). + +The `NicknamePokemon` task will rename all pokemon in inventory on startup to match the given template and will rename any newly caught/hatched/evolved pokemon as the bot runs. _It may take one or two "ticks" after catching/hatching/evolving a pokemon for it to be renamed. This is intended behavior._ + +> **NOTE:** If you experience frequent `Pokemon not found` error messages, this is because the inventory cache has not been updated after a pokemon was released. This can be remedied by placing the `NicknamePokemon` task above the `TransferPokemon` task in your `config.json` file. + +Niantic imposes a 12-character limit on all pokemon nicknames, so any new nickname will be truncated to 12 characters if over that limit. Thus, it is up to the user to exercise judgment on what template will best suit their need with this constraint in mind. + +Because some pokemon have very long names, you can use the [Format String syntax](https://docs.python.org/2.7/library/string.html#formatstrings) to ensure that your names do not cause your templates to truncate. For example, using `{name:.8s}` causes the Pokemon name to never take up more than 8 characters in the nickname. This would help guarantee that a template like `{name:.8s}_{iv_pct}` never goes over the 12-character limit. + +Valid names in templates are: +- `name` = pokemon name +- `id` = pokemon type id (e.g. 1 for Bulbasaurs) +- `cp` = pokemon's CP +- `iv_attack` = pokemon's attack IV +- `iv_defense` = pokemon's defense IV +- `iv_stamina` = pokemon's stamina IV +- `iv_ads` = pokemon's IVs in `(attack)/(defense)/(stamina)` format (matches web UI format -- A/D/S) +- `iv_sum` = pokemon's IVs as a sum (e.g. 45 when 3 perfect 15 IVs) +- `iv_pct` = pokemon's IVs as a percentage (0-100) + +> **NOTE:** Use a blank template (`""`) to revert all pokemon to their original names (as if they had no nickname). + +Sample usages: +- `"{name}_{iv_pct}"` => `Mankey_69` +- `"{iv_pct}_{iv_ads}"` => `91_15/11/15` +- `""` -> `Mankey` +![sample](https://cloud.githubusercontent.com/assets/8896778/17285954/0fa44a88-577b-11e6-8204-b1302f4294bd.png) + +## Sniping _(MoveToLocation)_ +### Description +This task will fetch current pokemon spawns from /raw_data of an PokemonGo-Map instance. For information on how to properly setup PokemonGo-Map have a look at the Github page of the project [here](https://github.com/AHAAAAAAA/PokemonGo-Map/). There is an example config in `config/config.json.map.example` + +### Options +* `Address` - Address of the webserver of PokemonGo-Map. ex: `http://localhost:5000` +* `Mode` - Which mode to run snipin on + - `distance` - Will move to the nearest pokemon + - `priority` - Will move to the pokemon with the highest priority assigned (tie breaking by distance) +* `prioritize_vips` - Will prioritize vips in distance and priority mode above all normal pokemon if set to true +* `min_time` - Minimum time the pokemon has to be available before despawn +* `max_distance` - Maximum distance the pokemon is allowed to be when walking, ignored when sniping +* `snipe`: + - `True` - Will teleport to target pokemon, encounter it, teleport back then catch it + - `False` - Will walk normally to the pokemon +* `update_map` - disable/enable if the map location should be automatically updated to the bots current location +* `catch` - A dictionary of pokemon to catch with an assigned priority (higher => better) +* `snipe_high_prio_only` - Whether to snipe pokemon above a certain threshold. +* `snipe_high_prio_threshold` - The threshold number corresponding with the `catch` dictionary. Any pokemon above this threshold will be caught. Other will be igonored. + +#### Example +``` +{ + \\ ... + { + "type": "MoveToMapPokemon", + "config": { + "address": "http://localhost:5000", + "max_distance": 500, + "min_time": 60, + "min_ball": 50, + "prioritize_vips": true, + "snipe": true, + "snipe_high_prio_only": true, + "snipe_high_prio_threshold": 400, + "update_map": true, + "mode": "priority", + "catch": { + "Aerodactyl": 1000, + "Ditto": 900, + "Omastar": 500, + "Omanyte": 150, + "Caterpie": 10, + } + } + } + \\ ... +} +``` diff --git a/docs/develop.md b/docs/develop.md new file mode 100644 index 0000000000..7a1649219f --- /dev/null +++ b/docs/develop.md @@ -0,0 +1,28 @@ +> $ git clone --recursive -b dev https://github.com/PokemonGoF/PokemonGo-Bot +> $ cd PokemonGo-Bot +> // create virtualenv using Python 2.7 executable +> $ virtualenv -p C:\python27\python.exe venv +> $ source venv/Scripts/activate +> $ pip install -r requirements.txt + +Once you are you to date with [dev-branch] (https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) create a pull request and it will be re-viewed + + +### How to add/discover new API +The example is [here](https://github.com/PokemonGoF/PokemonGo-Bot/commit/46e2352ce9f349cc127a408959679282f9999585) +1. Check the type of your API request in [POGOProtos](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/RequestType.proto) For example: RECYCLE_INVENTORY_ITEM +2. Convert to the api call in pokemongo_bot/__init__.py, RECYCLE_INVENTORY_ITEM change to self.api.recycle_inventory_item +``` +def drop_item(self,item_id,count): + self.api.recycle_inventory_item(...............) +``` +3. Where is the param list? +You need check this [Requests/Messages/RecycleInventoryItemMessage.proto](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/Messages/RecycleInventoryItemMessage.proto) +4. Then our final api call is +``` +def drop_item(self,item_id,count): + self.api.recycle_inventory_item(item_id=item_id,count=count) + inventory_req = self.api.call() + print(inventory_req) +``` +5. You can now debug on the log to see if get what you need diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000000..04823a2f5c --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,25 @@ +Start by downloading for your platform: [Mac](https://www.docker.com/products/docker#/mac), [Windows](https://www.docker.com/products/docker#/windows), or [Linux](https://www.docker.com/products/docker#/linux). Once you have Docker installed, simply create the various config.json files for your different accounts (e.g. `configs/config-account1.json`) and then create a Docker image for PokemonGo-Bot using the Dockerfile in this repo. +``` +cd PokemonGo-Bot +docker build -t pokemongo-bot . +``` +You can verify that the image was created with: +``` +docker images +``` + +To run PokemonGo-Bot Docker image you've created, simple run: +``` +docker run --name=pokego-bot1 --rm -it -v $(pwd)/configs/config-account1.json:/usr/src/app/configs/config.json pokemongo-bot +``` +_Check the logs in real-time `docker logs -f pgobot`_ + +If you want to run multiple accounts with the same Docker image, simply specify different config.json and names in the Docker run command. +Do not push your image to a registry with your config.json and account details in it! + +Share web folder with host: +``` +docker run -it -v $(pwd)/web/:/usr/src/app/web --rm --name=pgo-bot-acct1 pokemongo-bot --config config.json +``` + +TODO: Add configuration for running multiple Docker containers from the same image for every bot instance, and a single container for the web UI. diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000000..7720f0a64c --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,49 @@ +### How do I start the application? +After customizing your config.json files, cd to the PokemonGo-Bot folder and enter: +``` +$ python pokecli.py +``` +This will start the application. + +### Python possible bug +If you encounter problems with the module `ssl` and it's function `_create_unverified_context`, just comment it. (Solution available in Python 2.7.11) +In order to comment out the function and the module, please follow the instructions below: +- edit `pokecli.py` +- put `#` before `if` (line 43) and `ssl` (line 44) +- save it + +Please keep in mind that this fix is only necessary if your python version don't have the `_create_unverified_context` argument in the ssl module. + +### What's IV? +Here's the [introduction](http://bulbapedia.bulbagarden.net/wiki/Individual_values) + +### Does it run automatically? +Not yet, still need a trainer to train the script param. But we are very close to. + +### Set GEO Location +It works, use "location": "59.333409,18.045008", in configs/config.json to set lat long for location. Use a Pokemon Go map to find an area with pokemons you still need (e.g. [https://pokevision.com/](https://pokevision.com/)), however don't jump too big distances (see "softban"). + +### Google login issues (Login Error, Server busy)? +Try to generate an [app password](!https://support.google.com/accounts/answer/185833?hl=en) and set is as +``` +-p "" +``` +This error mostly occurs for those who are using 2 factor authentication, but either way, for the purpose of security it would be nice to have a separate password for the bot app. + +### FLEE +The status code "3" corresponds to "Flee" - meaning your Pokemon has ran away. + {"responses": { "CATCH_POKEMON": { "status": 3 } } + +### My pokemon are not showing up in my Pokedex? +Finish the tutorial on a smartphone. This will then allow everything to be visible. + +### How can I maximise my XP per hour? +Quick Tip: When using this script, use a Lucky egg to double the XP for 30 mins. You will level up much faster. A Lucky egg is obtained on level 9 and further on whilst leveling up. (from VipsForever via /r/pokemongodev) + +### How do I use the map?? +[See wiki info here] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page)) + +### No JSON object could be decoded or decoder.py error +If you see "No JSON object could be decoded" or you see "decoder.py" in the last part of the error, this means that there is something wrong with your JSON. + +Copy the json in json files and copy it into http://jsonlint.com/ Then fix the error it gives you in your json. diff --git a/docs/google_map.md b/docs/google_map.md new file mode 100644 index 0000000000..7c6e3aca6c --- /dev/null +++ b/docs/google_map.md @@ -0,0 +1,54 @@ +The webpage is a submodule to this repository and config related to that is in ./web folder + +[OpenPoGoWeb] (https://github.com/OpenPoGo/OpenPoGoWeb) uses Google Maps. Read their [README] (https://github.com/OpenPoGo/OpenPoGoWeb/blob/master/README.md) for how to configure web frontend + +## How to set up a simple webserver with nginx +## SimpleHTTPServer +You can either view the map via opening the html file, or by serving it with SimpleHTTPServer (runs on localhost:8000) +To use SimpleHTTPServer: +``` +$ python -m SimpleHTTPServer [port] +``` +The default port is 8000, you can change that by giving a port number. Anything above port 1000 does not require root. +You will need to set your username(s) in the userdata.js file before opening, **Copy userdata.js.example to userdata.js** and edit with your favorite text editor. Put your username in the quotes instead of "username" +If using multiple usernames format like this +``` +var users = ["username1","username2"]; +``` +On Windows you can now go to http://127.0.0.1:8000 to see the map + + + +### Nginx on Ubuntu 14.x, 16.x +#### 1. Install nginx on your Ubuntu machine (e.g. on locally or AWS) +``` +sudo apt-get update +sudo apt-get install nginx +``` + +#### 2. Check the webserver +Check if the webserver is running by using your browser and entering the IP address of your local machine/server. +On a local machine this would be http://127.0.0.1. On AWS this is your public DNS if you haven't configured an elastic IP. + +#### 3. Change Base Directory of the Webserver +``` +sudo nano "/etc/nginx/sites-enabled/default" +``` +Comment out following line: ```root /var/www/html;``` and change it to the web folder of your PokemonGo-Bot: eg: +``` +/home/user/dev/PokemonGo-Bot/web; +``` +Use `nginx -s reload` to load the new configurations. + + +*** +Common Errors and Solutions + +> missing files: 127.0.0.1 - - "GET /catchable-YOURACCOUNT.json 404 +and location-SOMEACCOUNT.json 404 + +just create the file catachable-someaccount@gmail.com.json and put +``` +{} +``` +save and close repeat for other file. (location-SOMEACCOUNT.json) diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000000..8f5f5692e3 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,93 @@ +### Requirements (click each one for install guide) + +- [Python 2.7.x](http://docs.python-guide.org/en/latest/starting/installation/) +- [pip](https://pip.pypa.io/en/stable/installing/) +- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) +- [docker](https://docs.docker.com/engine/installation/) (Optional) - [how to setup after installation](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/How-to-run-with-Docker) +- [protobuf 3](https://github.com/google/protobuf) (OS Dependent, see below) + +### Protobuf 3 installation + +- OS X: `brew update && brew install --devel protobuf` +- Windows: Download protobuf 3.0: [here](https://github.com/google/protobuf/releases/download/v3.0.0-beta-4/protoc-3.0.0-beta-4-win32.zip) and unzip `bin/protoc.exe` into a folder in your PATH. +- Linux: `apt-get install python-protobuf` + +### Get encrypt.so (Windows part writing need fine tune) +We don't have the copyright of encrypt.so, please grab from internet and build your self.Take the risk as your own. +Example build sequence: +Create a new separate folder some here + +wget http://pgoapi.com/pgoencrypt.tar.gz && tar -xf pgoencrypt.tar.gz && cd pgoencrypt/src/ && make +Then copy libencrypt.so to the gofbot folder and rename to encrypt.so + +### Note on branch +Please keep in mind that master is not always up-to-date whereas 'dev' is. In the installation note below change `master` to `dev` if you want to get and use the latest version. + +## Update +To update your project do (in the project folder): `git pull` + +To update python requirement packages do (in the project folder): `pip install --upgrade -r requirements.txt` + +### Installation Linux +(change master to dev for the latest version) + +``` +$ git clone --recursive -b master https://github.com/PokemonGoF/PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ source bin/activate +$ pip install -r requirements.txt +``` +#### Example Installation for Ubuntu +(change dev to master for the lastest master version) + +http://pastebin.com/pzPjXT65 + + +### Installation Mac +(change master to dev for the latest version) + +``` +$ git clone --recursive -b master https://github.com/PokemonGoF/PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ source bin/activate +$ pip install -r requirements.txt +``` + +### Installation Windows +(change master to dev for the latest version) + +On Windows, you will need to install PyYaml through the installer and not through requirements.txt. + +##### Windows vista, 7, 8: +Go to : http://pyyaml.org/wiki/PyYAML , download the right version for your pc and install it + +##### Windows 10: +Go to [this](http://www.lfd.uci.edu/~gohlke/pythonlibs/#pyyaml) page and download: PyYAML-3.11-cp27-cp27m-win32.whl +(If running 64-bit python or if you get a 'not a supported wheel on this platform' error, +download the 64 bit version instead: PyYAML-3.11-cp27-cp27m-win_amd64.whl ) + +*(Run the following commands from Git Bash.)* + +``` +// switch to the directory where you downloaded PyYAML +$ cd download-directory +// install 32-bit version +$ pip2 install PyYAML-3.11-cp27-cp27m-win32.whl +// if you need to install the 64-bit version, do this instead: +// pip2 install PyYAML-3.11-cp27-cp27m-win_amd64.whl +``` + +After this, just do: + +``` +$ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ script\activate +$ pip2 install -r requirements.txt +$ git submodule init +$ git submodule update +``` diff --git a/docs/pokemon_iv.md b/docs/pokemon_iv.md new file mode 100644 index 0000000000..8662314fd5 --- /dev/null +++ b/docs/pokemon_iv.md @@ -0,0 +1,58 @@ +Individual Values, or IVs function like a Pokémon's "Genes". They are the traits which are passed down from one generation to the next. + +![](http://vignette3.wikia.nocookie.net/pokemon/images/d/dd/ImagesCAD6WL01.jpg/revision/latest?cb=20110511020243) + +**Individual values** + +Every stat has an IV ranging from 0 to 31 for each stat (HP, ATK, DEF, SPA, SPD, and SPE), and at level 100, their IVs are added to the Pokémon's stats for their total values. For example, a level 100 Tyranitar with no Effort Values and 0 IVs has 310 HP, however if it had 31 IVs, it would have 341 HP. + + +These stats are provided randomly for every Pokémon, caught or bred, and although as insignificant as 31 points may seem, they are required for Ace Trainers to obtain when breeding Pokémon with perfect natures/stats. On some occasions they are even the tipping point in a close matchup. For example, if there was a Terrakion with 0 Attack IV, it will have an attack of 358 at level 100 (with an attack improving nature), while a Terrakion with perfect Attack IVs would have 392 Attack. This small difference can mean the difference between a one-hit kill (not an OHKO) and survival with 1 HP. + + +**Breeding IVs** +Fortunately for trainers, Ace Trainers and Pokémon Breeders especially, IVs can be bred to obtain the perfect Pokémon. + +The process of breeding IVs is as follows, the example displayed below is to breed Nidorans: + +* The child's IV's are generated randomly, for example: 7/27/31/14/19/2, in HP/ATK/DEF/SPA/SPD/SPE format. +* Three stats are inherited from the parents, and are selected in three checks: +1. First check: A random stat (HP/ATK/DEF/SPA/SPD/SPE) is selected from either the Mother or the Father and passed on to the child. +2. Second check: A random stat with the exception of HP (ATK/DEF/SPA/SPD/SPE) is selected from either the Mother or the Father and passed on to the child. +3. Third check: A random stat with the exception of HP and DEF (ATK/SPA/SPD/SPE) is selected from either the Mother or the Father and passed on to the child. +This means that HP and DEF are less likely to pass on to the child, however there are ways to make sure the IVs are passed on. + +Letting either one of the parents hold a Power Item can ensure that the Power Item's respective stat will be passed on to the offspring from the parent that holds it. + +If the Power Item called Power Weight (doubles all HP EV gained) is held by a parent with a perfect IV of 31 for HP and the first check selects this parent, the child is ensured to have a perfect IV for HP. The other checks, though, will be random, and either luck or patience is required to eventually get the desired stats. + +Important: Only three stats are inherited per Pokémon, and these can stack. For example, the DEF IV can be inherited from both parents, thus rendering one redundant. + + + +**Checking IVs** +Beginning in Generation III, there has always been an NPC that allows players to check the IVs of their Pokémon. + + +If you wanted to check the IV's yourself the formula is as follows: + +The formula for HP is different from the rest of the stats, so here is the formula for HP: + +> IV=((Stat - Level Value - 10) * 100 / Level Value) - 2 * Base stat - (Math.Floor(EV/4)) + +In layman terms: + +> Individual Value= ((Current Stat Level - Current Level Value - 10) * 100 / Current Level Value) - 2 * Base Stat - (Math.Floor(EV/4)) + +Just in case you don't know (Math.Floor(EV/4)) means to take the amount of EVs you have in HP and divide it by 4 and then round down. +The formula you use for the rest of the stats is the same, so here it is: + +> IV=((Math.Ceiling(Stat/Nature) - 5) * 100 / Level Value) - 2 * Base Stat - (Math.Floor(EV/4)) + +In layman terms: + +> Individual Value= (Math.Ceiling(Current Stat Value/Nature Bonus) * 100 / Current Level Value) - 2 * Base Stat - (Math.Floor(EV/4)) + +Just in case you don't know (Math.Floor(EV/4)) means to take the amount of EVs you have in HP and divide it by 4 and then round down. + +Just in case you don't know (Math.Ceiling(Current Stat Value/Nature Bonus)) means to take the Current Stat Value and divide it by the bonus you get from the Pokémon's nature and then round up. If the stat gets an increase from the nature you divide the Current Stat Value by 1.1, and if it is a decrease from the nature you divide the Current Stat Value by 0.9. diff --git a/install.sh b/install.sh index 32cc1e1124..1e7415ed30 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -pokebotpath=$(pwd) +pokebotpath=$(cd "$(dirname "$0")"; pwd) cd $pokebotpath if [ -f /etc/debian_version ] then @@ -17,7 +17,9 @@ echo "You are on Mac os" sudo brew update sudo brew install --devel protobuf else -echo "Nothing happend." +echo "Please check if you have python pip protobuf gcc make installed on your device." +echo "Wait 5 seconds to continue or Use ctrl+c to interrupt this shell." +sleep 5 fi pip install virtualenv cd $pokebotpath @@ -36,7 +38,7 @@ mv libencrypt.so $pokebotpath/encrypt.so cd ../.. rm -rf pgoencrypt.tar.gz rm -rf pgoencrypt -echo "Install complete." +echo "Install complete. Starting to generate config.json." cd $pokebotpath read -p "1.google 2.ptc " auth @@ -58,5 +60,5 @@ sed -i "s/YOUR_USERNAME/$username/g" configs/config.json sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json sed -i "s/SOME_LOCATION/$location/g" configs/config.json sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json -echo "Edit configs/config.json to modify any other config. Use run.sh to run." +echo "Edit configs/config.json to modify any other config. Use run.sh ./configs/config.json to run." exit 0 diff --git a/pokecli.py b/pokecli.py index 55afa55399..f59ae3acb9 100644 --- a/pokecli.py +++ b/pokecli.py @@ -39,6 +39,7 @@ from geopy.exc import GeocoderQuotaExceeded from pokemongo_bot import PokemonGoBot, TreeConfigBuilder +from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.health_record import BotEvent from pokemongo_bot.plugin_loader import PluginLoader @@ -162,7 +163,7 @@ def report_summary(bot): def init_config(): parser = argparse.ArgumentParser() - config_file = "configs/config.json" + config_file = os.path.join(_base_dir, 'configs', 'config.json') web_dir = "web" # If config file exists, load variables from json @@ -394,6 +395,14 @@ def _json_loader(filename): type=float, default=5.0 ) + add_config( + parser, + load, + long_flag="--logging_color", + help="If logging_color is set to true, colorized logging handler will be used", + type=bool, + default=True + ) # Start to parse other attrs config = parser.parse_args() diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index dd4de22551..1b18ac7ff8 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -25,9 +25,10 @@ from human_behaviour import sleep from item_list import Item from metrics import Metrics -from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler +from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler, ColoredLoggingHandler from pokemongo_bot.socketio_server.runner import SocketIoRunner from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl +from pokemongo_bot.base_dir import _base_dir from worker_result import WorkerResult from tree_config_builder import ConfigException, MismatchTaskApiVersion, TreeConfigBuilder from inventory import init_inventory @@ -57,9 +58,9 @@ def __init__(self, config): self.config = config self.fort_timeouts = dict() self.pokemon_list = json.load( - open(os.path.join('data', 'pokemon.json')) + open(os.path.join(_base_dir, 'data', 'pokemon.json')) ) - self.item_list = json.load(open(os.path.join('data', 'items.json'))) + self.item_list = json.load(open(os.path.join(_base_dir, 'data', 'items.json'))) self.metrics = Metrics(self) self.latest_inventory = None self.cell = None @@ -87,7 +88,12 @@ def start(self): random.seed() def _setup_event_system(self): - handlers = [LoggingHandler()] + handlers = [] + if self.config.logging_color: + handlers.append(ColoredLoggingHandler()) + else: + handlers.append(LoggingHandler()) + if self.config.websocket_server_url: if self.config.websocket_start_embedded_server: self.sio_runner = SocketIoRunner(self.config.websocket_server_url) @@ -102,19 +108,18 @@ def _setup_event_system(self): if self.config.websocket_remote_control: remote_control = WebsocketRemoteControl(self).start() - self.event_manager = EventManager(*handlers) self._register_events() if self.config.show_events: self.event_manager.event_report() sys.exit(1) - # Registering event: - # self.event_manager.register_event("location", parameters=['lat', 'lng']) - # - # Emitting event should be enough to add logging and send websocket - # message: : - # self.event_manager.emit('location', 'level'='info', data={'lat': 1, 'lng':1}), + # Registering event: + # self.event_manager.register_event("location", parameters=['lat', 'lng']) + # + # Emitting event should be enough to add logging and send websocket + # message: : + # self.event_manager.emit('location', 'level'='info', data={'lat': 1, 'lng':1}), def _register_events(self): self.event_manager.register_event( @@ -233,6 +238,10 @@ def _register_events(self): 'cp', 'iv', 'iv_display', + 'encounter_id', + 'latitude', + 'longitude', + 'pokemon_id' ) ) self.event_manager.register_event('no_pokeballs') @@ -267,7 +276,13 @@ def _register_events(self): ) self.event_manager.register_event( 'pokemon_vanished', - parameters=('pokemon',) + parameters=( + 'pokemon', + 'encounter_id', + 'latitude', + 'longitude', + 'pokemon_id' + ) ) self.event_manager.register_event('pokemon_not_in_range') self.event_manager.register_event('pokemon_inventory_full') @@ -275,7 +290,11 @@ def _register_events(self): 'pokemon_caught', parameters=( 'pokemon', - 'cp', 'iv', 'iv_display', 'exp' + 'cp', 'iv', 'iv_display', 'exp', + 'encounter_id', + 'latitude', + 'longitude', + 'pokemon_id' ) ) self.event_manager.register_event( @@ -498,12 +517,12 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): location = self.position[0:2] cells = self.find_close_cells(*location) - user_data_cells = "data/cells-%s.json" % self.config.username + user_data_cells = os.path.join(_base_dir, 'data', 'cells-%s.json' % self.config.username) with open(user_data_cells, 'w') as outfile: json.dump(cells, outfile) user_web_location = os.path.join( - 'web', 'location-%s.json' % self.config.username + _base_dir, 'web', 'location-%s.json' % self.config.username ) # alt is unused atm but makes using *location easier try: @@ -518,7 +537,7 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): self.logger.info('[x] Error while opening location file: %s' % e) user_data_lastlocation = os.path.join( - 'data', 'last-location-%s.json' % self.config.username + _base_dir, 'data', 'last-location-%s.json' % self.config.username ) try: with open(user_data_lastlocation, 'w') as outfile: @@ -790,7 +809,7 @@ def current_inventory(self): inventory_dict = inventory_req['responses']['GET_INVENTORY'][ 'inventory_delta']['inventory_items'] - user_web_inventory = 'web/inventory-%s.json' % self.config.username + user_web_inventory = os.path.join(_base_dir, 'web', 'inventory-%s.json' % self.config.username) with open(user_web_inventory, 'w') as outfile: json.dump(inventory_dict, outfile) @@ -901,8 +920,8 @@ def _set_starting_position(self): level='debug', formatted='Loading cached location...' ) - with open('data/last-location-%s.json' % - self.config.username) as f: + with open(os.path.join(_base_dir, 'data', 'last-location-%s.json' % + self.config.username)) as f: location_json = json.load(f) location = ( location_json['lat'], @@ -987,9 +1006,10 @@ def heartbeat(self): pass def update_web_location_worker(self): - while True: - self.web_update_queue.get() - self.update_web_location() + pass + # while True: + # self.web_update_queue.get() + # self.update_web_location() def get_inventory_count(self, what): response_dict = self.get_inventory() @@ -1051,8 +1071,8 @@ def has_space_for_loot(self): def get_forts(self, order_by_distance=False): forts = [fort - for fort in self.cell['forts'] - if 'latitude' in fort and 'type' in fort] + for fort in self.cell['forts'] + if 'latitude' in fort and 'type' in fort] if order_by_distance: forts.sort(key=lambda x: distance( diff --git a/pokemongo_bot/base_dir.py b/pokemongo_bot/base_dir.py new file mode 100644 index 0000000000..83978c964a --- /dev/null +++ b/pokemongo_bot/base_dir.py @@ -0,0 +1,4 @@ +import os + + +_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') diff --git a/pokemongo_bot/base_task.py b/pokemongo_bot/base_task.py index 22bbedf4e8..1b610d31aa 100644 --- a/pokemongo_bot/base_task.py +++ b/pokemongo_bot/base_task.py @@ -9,6 +9,7 @@ def __init__(self, bot, config): self.config = config self._validate_work_exists() self.logger = logging.getLogger(type(self).__name__) + self.enabled = config.get('enabled', True) self.initialize() def _validate_work_exists(self): diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py index 654c2467b3..0203459c28 100644 --- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -1,9 +1,11 @@ import json +import os from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker from utils import distance from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.base_dir import _base_dir class CatchVisiblePokemon(BaseTask): @@ -27,7 +29,7 @@ def work(self): key= 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) + user_web_catchable = os.path.join(_base_dir, '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) diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 4cc451115b..7380f1c5db 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -117,8 +117,3 @@ def _execute_pokemon_evolve(self, pokemon, cache): cache[pokemon.name] = 1 sleep(0.7) return False - - def _compute_iv(self, pokemon): - total_iv = pokemon.get("individual_attack", 0) + pokemon.get("individual_stamina", 0) + pokemon.get( - "individual_defense", 0) - return round((total_iv / 45.0), 2) diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 7dcd0977b1..24ecf5e74a 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -13,13 +13,17 @@ 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.ignore_item_count = self.config.get("ignore_item_count", False) + if self.config: + 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) + else: + self.lure_attraction = None + self.ignore_item_count = True def should_run(self): has_space_for_loot = self.bot.has_space_for_loot() - if not has_space_for_loot: + if not has_space_for_loot and not self.ignore_item_count: self.emit_event( 'inventory_full', formatted="Inventory is full. You might want to change your config to recycle more items if this message appears consistently." diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 2cfd45d14b..efd058ca96 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -54,6 +54,7 @@ import json import base64 import requests +from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.cell_workers.utils import distance, format_dist, format_time from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult @@ -83,8 +84,9 @@ def initialize(self): self.unit = self.bot.config.distance_unit self.caught = [] self.min_ball = self.config.get('min_ball', 1) + self.map_path = self.config.get('map_path', 'raw_data') - data_file = 'data/map-caught-{}.json'.format(self.bot.config.username) + data_file = os.path.join(_base_dir, 'map-caught-{}.json'.format(self.bot.config.username)) if os.path.isfile(data_file): self.caught = json.load( open(data_file) @@ -92,7 +94,7 @@ def initialize(self): def get_pokemon_from_map(self): try: - req = requests.get('{}/raw_data?gyms=false&scanned=false'.format(self.config['address'])) + req = requests.get('{}/{}?gyms=false&scanned=false'.format(self.config['address'], self.map_path)) except requests.exceptions.ConnectionError: self._emit_failure('Could not get Pokemon data from PokemonGo-Map: ' '{}. Is it running?'.format( @@ -222,7 +224,7 @@ def snipe(self, pokemon): return WorkerResult.SUCCESS def dump_caught_pokemon(self): - user_data_map_caught = 'data/map-caught-{}.json'.format(self.bot.config.username) + user_data_map_caught = os.path.join(_base_dir, 'data', 'map-caught-{}.json'.format(self.bot.config.username)) with open(user_data_map_caught, 'w') as outfile: json.dump(self.caught, outfile) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index d551a68632..3b2092e535 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -98,6 +98,10 @@ def work(self, response_dict=None): 'cp': pokemon.cp, 'iv': pokemon.iv, 'iv_display': pokemon.iv_display, + 'encounter_id': self.pokemon['encounter_id'], + 'latitude': self.pokemon['latitude'], + 'longitude': self.pokemon['longitude'], + 'pokemon_id': pokemon.num } ) @@ -247,7 +251,7 @@ def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, cu data={ 'berry_name': self.item_list[str(berry_id)], 'ball_name': self.item_list[str(current_ball)], - 'new_catch_rate': self._pct(catch_rate_by_ball[current_ball]) + 'new_catch_rate': self._pct(new_catch_rate_by_ball[current_ball]) } ) @@ -370,7 +374,13 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): self.emit_event( 'pokemon_vanished', formatted='{pokemon} vanished!', - data={'pokemon': pokemon.name} + data={ + 'pokemon': pokemon.name, + 'encounter_id': self.pokemon['encounter_id'], + 'latitude': self.pokemon['latitude'], + 'longitude': self.pokemon['longitude'], + 'pokemon_id': pokemon.num + } ) if self._pct(catch_rate_by_ball[current_ball]) == 100: self.bot.softban = True @@ -386,7 +396,11 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): 'cp': pokemon.cp, 'iv': pokemon.iv, 'iv_display': pokemon.iv_display, - 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) + 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']), + 'encounter_id': self.pokemon['encounter_id'], + 'latitude': self.pokemon['latitude'], + 'longitude': self.pokemon['longitude'], + 'pokemon_id': pokemon.num } ) diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 673b373fba..3232870d03 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -1,5 +1,6 @@ import json import os +from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.base_task import BaseTask from pokemongo_bot.tree_config_builder import ConfigException @@ -13,7 +14,7 @@ def initialize(self): self._validate_item_filter() def _validate_item_filter(self): - item_list = json.load(open(os.path.join('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(): if config_item_name not in item_list.viewvalues(): if config_item_name not in item_list: @@ -27,7 +28,7 @@ def work(self): 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: + 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.", diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 445946e7e1..9422d8ec35 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -19,7 +19,7 @@ def initialize(self): self.ignore_item_count = self.config.get("ignore_item_count", False) def should_run(self): - if not self.bot.has_space_for_loot(): + if not self.bot.has_space_for_loot() and not self.ignore_item_count: self.emit_event( 'inventory_full', formatted="Inventory is full. You might want to change your config to recycle more items if this message appears consistently." @@ -106,10 +106,11 @@ def work(self): data={'pokestop': fort_name, 'minutes_left': minutes_left} ) elif spin_result == 4: - self.emit_event( - 'inventory_full', - formatted="Inventory is full!" - ) + if not self.ignore_item_count: + self.emit_event( + 'inventory_full', + formatted="Inventory is full!" + ) else: self.emit_event( 'unknown_spin_result', diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index ebc197ef24..9e970d7d7f 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -1,7 +1,10 @@ import json +import os +from pokemongo_bot import inventory from pokemongo_bot.human_behaviour import action_delay from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.inventory import Pokemons class TransferPokemon(BaseTask): @@ -9,129 +12,75 @@ class TransferPokemon(BaseTask): def work(self): pokemon_groups = self._release_pokemon_get_groups() - for pokemon_id in pokemon_groups: - group = pokemon_groups[pokemon_id] - - if len(group) > 0: - pokemon_name = self.bot.pokemon_list[pokemon_id - 1]['Name'] - keep_best, keep_best_cp, keep_best_iv = self._validate_keep_best_config(pokemon_name) - - if keep_best: - best_pokemon_ids = set() - order_criteria = 'none' - if keep_best_cp >= 1: - cp_limit = keep_best_cp - best_cp_pokemons = sorted(group, key=lambda x: (x['cp'], x['iv']), reverse=True)[:cp_limit] - best_pokemon_ids = set(pokemon['pokemon_data']['id'] for pokemon in best_cp_pokemons) - order_criteria = 'cp' - - if keep_best_iv >= 1: - iv_limit = keep_best_iv - best_iv_pokemons = sorted(group, key=lambda x: (x['iv'], x['cp']), reverse=True)[:iv_limit] - best_pokemon_ids |= set(pokemon['pokemon_data']['id'] for pokemon in best_iv_pokemons) - if order_criteria == 'cp': - order_criteria = 'cp and iv' - else: - order_criteria = 'iv' - - # remove best pokemons from all pokemons array - all_pokemons = group - best_pokemons = [] - for best_pokemon_id in best_pokemon_ids: - for pokemon in all_pokemons: - if best_pokemon_id == pokemon['pokemon_data']['id']: - all_pokemons.remove(pokemon) - best_pokemons.append(pokemon) - - transfer_pokemons = [pokemon for pokemon in all_pokemons - if self.should_release_pokemon(pokemon_name, - pokemon['cp'], - pokemon['iv'], - True)] - - if transfer_pokemons: - if best_pokemons: - self.emit_event( - 'keep_best_release', - formatted="Keeping best {amount} {pokemon}, based on {criteria}", - data={ - 'amount': len(best_pokemons), - 'pokemon': pokemon_name, - 'criteria': order_criteria - } - ) - for pokemon in transfer_pokemons: - self.release_pokemon(pokemon_name, pokemon['cp'], pokemon['iv'], pokemon['pokemon_data']['id']) - else: - group = sorted(group, key=lambda x: x['cp'], reverse=True) - for item in group: - pokemon_cp = item['cp'] - pokemon_potential = item['iv'] - - if self.should_release_pokemon(pokemon_name, pokemon_cp, pokemon_potential): - self.release_pokemon(pokemon_name, item['cp'], item['iv'], item['pokemon_data']['id']) + for pokemon_id, group in pokemon_groups.iteritems(): + pokemon_name = Pokemons.name_for(pokemon_id) + keep_best, keep_best_cp, keep_best_iv = self._validate_keep_best_config(pokemon_name) + + if keep_best: + best_pokemon_ids = set() + order_criteria = 'none' + if keep_best_cp >= 1: + cp_limit = keep_best_cp + best_cp_pokemons = sorted(group, key=lambda x: (x.cp, x.iv), reverse=True)[:cp_limit] + best_pokemon_ids = set(pokemon.id for pokemon in best_cp_pokemons) + order_criteria = 'cp' + + if keep_best_iv >= 1: + iv_limit = keep_best_iv + best_iv_pokemons = sorted(group, key=lambda x: (x.iv, x.cp), reverse=True)[:iv_limit] + best_pokemon_ids |= set(pokemon.id for pokemon in best_iv_pokemons) + if order_criteria == 'cp': + order_criteria = 'cp and iv' + else: + order_criteria = 'iv' + + # remove best pokemons from all pokemons array + all_pokemons = group + best_pokemons = [] + for best_pokemon_id in best_pokemon_ids: + for pokemon in all_pokemons: + if best_pokemon_id == pokemon.id: + all_pokemons.remove(pokemon) + best_pokemons.append(pokemon) + + transfer_pokemons = [pokemon for pokemon in all_pokemons if self.should_release_pokemon(pokemon,True)] + + if transfer_pokemons: + if best_pokemons: + self.emit_event( + 'keep_best_release', + formatted="Keeping best {amount} {pokemon}, based on {criteria}", + data={ + 'amount': len(best_pokemons), + 'pokemon': pokemon_name, + 'criteria': order_criteria + } + ) + for pokemon in transfer_pokemons: + self.release_pokemon(pokemon) + else: + group = sorted(group, key=lambda x: x.cp, reverse=True) + for pokemon in group: + if self.should_release_pokemon(pokemon): + self.release_pokemon(pokemon) def _release_pokemon_get_groups(self): pokemon_groups = {} - 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 - - inventory_dict = inventory_req['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] - - user_web_inventory = 'web/inventory-%s.json' % (self.bot.config.username) - with open(user_web_inventory, 'w') as outfile: - json.dump(inventory_dict, outfile) - - for pokemon in inventory_dict: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data", "pokemon_id" - ], pokemon) - except KeyError: - continue - - pokemon_data = pokemon['inventory_item_data']['pokemon_data'] - - # pokemon in fort, so we cant transfer it - if 'deployed_fort_id' in pokemon_data and pokemon_data['deployed_fort_id']: - continue - - # favorite pokemon can't transfer in official game client - if pokemon_data.get('favorite', 0) is 1: + for pokemon in inventory.pokemons(True).all(): + if pokemon.in_fort or pokemon.is_favorite: continue - group_id = pokemon_data['pokemon_id'] - group_pokemon_cp = pokemon_data['cp'] - group_pokemon_iv = self.get_pokemon_potential(pokemon_data) + group_id = pokemon.pokemon_id if group_id not in pokemon_groups: pokemon_groups[group_id] = [] - pokemon_groups[group_id].append({ - 'cp': group_pokemon_cp, - 'iv': group_pokemon_iv, - 'pokemon_data': pokemon_data - }) + pokemon_groups[group_id].append(pokemon) return pokemon_groups - def get_pokemon_potential(self, pokemon_data): - total_iv = 0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - for individual_stat in iv_stats: - try: - total_iv += pokemon_data[individual_stat] - except Exception: - continue - return round((total_iv / 45.0), 2) - - def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): - release_config = self._get_release_config_for(pokemon_name) + def should_release_pokemon(self, pokemon, keep_best_mode = False): + release_config = self._get_release_config_for(pokemon.name) if (keep_best_mode and not release_config.has_key('never_release') @@ -156,11 +105,11 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): return True release_cp = release_config.get('release_below_cp', 0) - if cp < release_cp: + if pokemon.cp < release_cp: release_results['cp'] = True release_iv = release_config.get('release_below_iv', 0) - if iv < release_iv: + if pokemon.iv < release_iv: release_results['iv'] = True logic_to_function = { @@ -171,11 +120,11 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): if logic_to_function[cp_iv_logic](*release_results.values()): self.emit_event( 'future_pokemon_release', - formatted="Releasing {pokemon} (CP {cp}/IV {iv}) based on rule: CP < {below_cp} {cp_iv_logic} IV < {below_iv}", + formatted="Releasing {pokemon} [CP {cp}] [IV {iv}] based on rule: CP < {below_cp} {cp_iv_logic} IV < {below_iv}", data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': iv, + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv, 'below_cp': release_cp, 'cp_iv_logic': cp_iv_logic.upper(), 'below_iv': release_iv @@ -184,16 +133,27 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): return logic_to_function[cp_iv_logic](*release_results.values()) - def release_pokemon(self, pokemon_name, cp, iv, pokemon_id): - response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon_id) + def release_pokemon(self, pokemon): + try: + if self.bot.config.test: + candy_awarded = 1 + else: + response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon.id) + candy_awarded = response_dict['responses']['RELEASE_POKEMON']['candy_awarded'] + except KeyError: + return + + # We could refresh here too, but adding 1 saves a inventory request + candy = inventory.candies().get(pokemon.pokemon_id) + candy.add(candy_awarded) self.bot.metrics.released_pokemon() self.emit_event( 'pokemon_release', formatted='Exchanged {pokemon} [CP {cp}] [IV {iv}] for candy.', data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': iv + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv } ) action_delay(self.bot.config.action_wait_min, self.bot.config.action_wait_max) diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index 0a6e3c592d..bc40ed82e8 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -18,10 +18,22 @@ class UpdateTitleStats(BaseTask): "type": "UpdateTitleStats", "config": { "min_interval": 10, - "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"] + "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"], } } + You can set a logging on terminal mode like this: + + Example logging on console (and disabling title change): + { + "type": "UpdateTitleStats", + "config": { + "min_interval": 10, + "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"], + "terminal_log": true, + "terminal_title": false + } + } Available stats : - login : The account login (from the credentials). - username : The trainer name (asked at first in-game connection). @@ -53,8 +65,6 @@ class UpdateTitleStats(BaseTask): """ SUPPORTED_TASK_API_VERSION = 1 - DEFAULT_MIN_INTERVAL = 10 - DEFAULT_DISPLAYED_STATS = [] def __init__(self, bot, config): """ @@ -67,12 +77,14 @@ def __init__(self, bot, config): super(UpdateTitleStats, self).__init__(bot, config) self.next_update = None - self.min_interval = self.DEFAULT_MIN_INTERVAL - self.displayed_stats = self.DEFAULT_DISPLAYED_STATS - self.bot.event_manager.register_event('update_title', parameters=('title')) + self.min_interval = int(self.config.get('min_interval', 120)) + self.displayed_stats = self.config.get('stats', []) + self.terminal_log = self.config.get('terminal_log', False) + self.terminal_title = self.config.get('terminal_title', True) - self._process_config() + self.bot.event_manager.register_event('update_title', parameters=('title',)) + self.bot.event_manager.register_event('log_stats',parameters=('title',)) def initialize(self): pass @@ -89,7 +101,12 @@ def work(self): # If title is empty, it couldn't be generated. if not title: return WorkerResult.SUCCESS - self._update_title(title, _platform) + + if self.terminal_title: + self._update_title(title, _platform) + + if self.terminal_log: + self._log_on_terminal(title) return WorkerResult.SUCCESS def _should_display(self): @@ -100,6 +117,16 @@ def _should_display(self): """ return self.next_update is None or datetime.now() >= self.next_update + def _log_on_terminal(self, title): + self.emit_event( + 'log_stats', + formatted="{title}", + data={ + 'title': title + } + ) + self.next_update = datetime.now() + timedelta(seconds=self.min_interval) + def _update_title(self, title, platform): """ Updates the window title using different methods, according to the given platform @@ -119,7 +146,7 @@ def _update_title(self, title, platform): 'title': title } ) - + if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) stdout.flush() @@ -133,14 +160,6 @@ def _update_title(self, title, platform): self.next_update = datetime.now() + timedelta(seconds=self.min_interval) - def _process_config(self): - """ - Fetches the configuration for this worker and stores the values internally. - :return: Nothing. - :rtype: None - """ - self.min_interval = int(self.config.get('min_interval', self.DEFAULT_MIN_INTERVAL)) - self.displayed_stats = self.config.get('stats', self.DEFAULT_DISPLAYED_STATS) def _get_stats_title(self, player_stats): """ diff --git a/pokemongo_bot/event_handlers/__init__.py b/pokemongo_bot/event_handlers/__init__.py index f0933a0e68..a4dd6fce7f 100644 --- a/pokemongo_bot/event_handlers/__init__.py +++ b/pokemongo_bot/event_handlers/__init__.py @@ -1,2 +1,3 @@ from logging_handler import LoggingHandler from socketio_handler import SocketIoHandler +from colored_logging_handler import ColoredLoggingHandler diff --git a/pokemongo_bot/event_handlers/colored_logging_handler.py b/pokemongo_bot/event_handlers/colored_logging_handler.py new file mode 100644 index 0000000000..f3c902d464 --- /dev/null +++ b/pokemongo_bot/event_handlers/colored_logging_handler.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import time +import sys +import struct + +from pokemongo_bot.event_manager import EventHandler + +class ColoredLoggingHandler(EventHandler): + EVENT_COLOR_MAP = { + 'api_error': 'red', + 'bot_exit': 'red', + 'bot_start': 'green', + 'config_error': 'red', + 'egg_already_incubating': 'yellow', + 'egg_hatched': 'green', + 'future_pokemon_release': 'yellow', + 'incubate': 'green', + 'incubator_already_used': 'yellow', + 'inventory_full': 'yellow', + 'item_discard_fail': 'red', + 'item_discarded': 'green', + 'keep_best_release': 'green', + 'level_up': 'green', + 'level_up_reward': 'green', + 'location_cache_error': 'yellow', + 'location_cache_ignored': 'yellow', + 'login_failed': 'red', + 'login_successful': 'green', + 'lucky_egg_error': 'red', + 'move_to_map_pokemon_encounter': 'green', + 'move_to_map_pokemon_fail': 'red', + 'next_egg_incubates': 'yellow', + 'next_sleep': 'green', + 'no_pokeballs': 'red', + 'pokemon_appeared': 'yellow', + 'pokemon_capture_failed': 'red', + 'pokemon_caught': 'blue', + 'pokemon_evolved': 'green', + 'pokemon_fled': 'red', + 'pokemon_inventory_full': 'red', + 'pokemon_nickname_invalid': 'red', + 'pokemon_not_in_range': 'yellow', + 'pokemon_release': 'green', + 'pokemon_vanished': 'red', + 'pokestop_empty': 'yellow', + 'pokestop_searching_too_often': 'yellow', + 'rename_pokemon': 'green', + 'skip_evolve': 'yellow', + 'softban': 'red', + 'spun_pokestop': 'cyan', + 'threw_berry_failed': 'red', + 'unknown_spin_result': 'red', + 'unset_pokemon_nickname': 'red', + 'vip_pokemon': 'red', + + # event names for 'white' still here to remember that these events are already determined its color. + 'arrived_at_cluster': 'white', + 'arrived_at_fort': 'white', + 'bot_sleep': 'white', + 'catchable_pokemon': 'white', + 'found_cluster': 'white', + 'incubate_try': 'white', + 'load_cached_location': 'white', + 'location_found': 'white', + 'login_started': 'white', + 'lured_pokemon_found': 'white', + 'move_to_map_pokemon_move_towards': 'white', + 'move_to_map_pokemon_teleport_back': 'white', + 'move_to_map_pokemon_updated_map': 'white', + 'moving_to_fort': 'white', + 'moving_to_lured_fort': 'white', + 'pokemon_catch_rate': 'white', + 'pokemon_evolve_fail': 'white', + 'pokestop_on_cooldown': 'white', + 'pokestop_out_of_range': 'white', + 'polyline_request': 'white', + 'position_update': 'white', + 'set_start_location': 'white', + 'softban_fix': 'white', + 'softban_fix_done': 'white', + 'spun_fort': 'white', + 'threw_berry': 'white', + 'threw_pokeball': 'white', + 'used_lucky_egg': 'white' + } + CONTINUOUS_EVENT_NAMES = [ + 'catchable_pokemon', + 'moving_to_lured_fort', + 'spun_fort' + ] + COLOR_CODE = { + 'red': '91', + 'green': '92', + 'yellow': '93', + 'blue': '94', + 'cyan': '96' + } + + def __init__(self): + self._last_event = None + try: + # this `try ... except` is for ImportError on Windows + import fcntl + import termios + self._ioctl = fcntl.ioctl + self._TIOCGWINSZ = termios.TIOCGWINSZ + except ImportError: + self._ioctl = None + self._TIOCGWINSZ = None + + def handle_event(self, event, sender, level, formatted_msg, data): + # Prepare message string + message = None + if formatted_msg: + try: + message = formatted_msg.decode('utf-8') + except UnicodeEncodeError: + message = formatted_msg + else: + message = '{}'.format(str(data)) + + # Replace message if necessary + if event == 'catchable_pokemon': + message = 'Something rustles nearby!' + + # Truncate previous line if same event continues + if event in ColoredLoggingHandler.CONTINUOUS_EVENT_NAMES and self._last_event == event and sys.stdout.isatty(): + # Filling with "' ' * terminal_width" in order to completely clear last line + terminal_width = self._terminal_width() + if terminal_width: + sys.stdout.write('\r{}\r'.format(' ' * terminal_width)) + else: + sys.stdout.write('\r') + else: + sys.stdout.write("\n") + + color_name = None + if event in ColoredLoggingHandler.EVENT_COLOR_MAP: + color_name = ColoredLoggingHandler.EVENT_COLOR_MAP[event] + + # Change color if necessary + if event == 'egg_hatched' and data.get('pokemon', 'error') == 'error': + # `egg_hatched` event will be dispatched in both cases: hatched pokemon info is successfully taken or not. + # change color from 'green' to 'red' in case of error. + color_name = 'red' + + if color_name in ColoredLoggingHandler.COLOR_CODE: + sys.stdout.write( + '[{time}] \033[{color}m{message}\033[0m'.format( + time=time.strftime("%H:%M:%S"), + color=ColoredLoggingHandler.COLOR_CODE[color_name], + message=message + ) + ) + else: + sys.stdout.write('[{time}] {message}'.format( + time=time.strftime("%H:%M:%S"), + message=message + )) + + sys.stdout.flush() + self._last_event = event + + def _terminal_width(self): + if self._ioctl is None or self._TIOCGWINSZ is None: + return None + + h, w, hp, wp = struct.unpack(str('HHHH'), + self._ioctl(0, self._TIOCGWINSZ, + struct.pack(str('HHHH'), 0, 0, 0, 0))) + return w diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index c74a85296f..d7f890933f 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1,25 +1,46 @@ import json +import logging import os +from pokemongo_bot.base_dir import _base_dir + ''' 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 + +# +# Abstraction + +class _StaticInventoryComponent(object): STATIC_DATA_FILE = None # optionally load static data from file, # dropping the data in a static variable named STATIC_DATA + STATIC_DATA = None 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)) + cls.STATIC_DATA = cls.process_static_data( + json.load(open(cls.STATIC_DATA_FILE))) + + @classmethod + def process_static_data(cls, data): + # optional hook for processing the static data + # default is to use the data directly + return data + + +class _BaseInventoryComponent(_StaticInventoryComponent): + TYPE = None # base key name for items of this type + ID_FIELD = None # identifier field for items of this type + + def __init__(self): + self._data = {} + super(_BaseInventoryComponent, self).__init__() def parse(self, item): # optional hook for parsing the dict for this item @@ -41,34 +62,22 @@ def retrieve_data(self, inventory): def refresh(self, inventory): self._data = self.retrieve_data(inventory) - def get(self, id): - return self._data(id) + def get(self, object_id): + return self._data.get(object_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 +# +# Inventory Components class Candies(_BaseInventoryComponent): TYPE = 'candy' ID_FIELD = 'family_id' @classmethod - def family_id_for(self, pokemon_id): + def family_id_for(cls, pokemon_id): return Pokemons.first_evolution_id_for(pokemon_id) def get(self, pokemon_id): @@ -96,7 +105,7 @@ def captured(self, pokemon_id): class Items(_BaseInventoryComponent): TYPE = 'item' ID_FIELD = 'item_id' - STATIC_DATA_FILE = os.path.join('data', 'items.json') + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'items.json') def count_for(self, item_id): return self._data[item_id]['count'] @@ -105,19 +114,122 @@ def count_for(self, item_id): class Pokemons(_BaseInventoryComponent): TYPE = 'pokemon_data' ID_FIELD = 'id' - STATIC_DATA_FILE = os.path.join('data', 'pokemon.json') + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'pokemon.json') - def parse(self, item): - if 'is_egg' in item: - return Egg(item) - return Pokemon(item) + @classmethod + def process_static_data(cls, data): + pokemon_id = 1 + for poke_info in data: + # prepare types + types = [poke_info['Type I'][0]] # required + for t in poke_info.get('Type II', []): + types.append(t) + poke_info['types'] = types + + # prepare attacks (moves) + cls._process_attacks(poke_info) + cls._process_attacks(poke_info, charged=True) + + # prepare movesets + poke_info['movesets'] = cls._process_movesets(poke_info, pokemon_id) + + # calculate maximum CP for the pokemon (best IVs, lvl 40) + base_attack = poke_info['BaseAttack'] + base_defense = poke_info['BaseDefense'] + base_stamina = poke_info['BaseStamina'] + max_cp = _calc_cp(base_attack, base_defense, base_stamina) + poke_info['max_cp'] = max_cp + + pokemon_id += 1 + return data + + @classmethod + def _process_movesets(cls, poke_info, pokemon_id): + # type: (dict, int) -> List[Moveset] + """ + The optimal moveset is the combination of two moves, one quick move + and one charge move, that deals the most damage over time. + + Because each quick move gains a certain amount of energy (different + for different moves) and each charge move requires a different amount + of energy to use, sometimes, a quick move with lower DPS will be + better since it charges the charge move faster. On the same note, + sometimes a charge move that has lower DPS will be more optimal since + it may require less energy or it may last for a longer period of time. + + Attacker have STAB (Same-type attack bonus - x1.25) pokemon have the + same type as attack. So we add it to the "Combo DPS" of the moveset. + + The defender attacks in intervals of 1 second for the first 2 attacks, + and then in intervals of 2 seconds for the remainder of the attacks. + This explains why we see two consecutive quick attacks at the beginning + of the match. As a result, we add +2 seconds to the DPS calculation + for defender DPS output. + + So to determine an optimal defensive moveset, we follow the same method + as we did for optimal offensive movesets, but instead calculate the + highest "Combo DPS" with an added 2 seconds to the quick move cool down. + + Note: critical hits have not yet been implemented in the game + + See http://pokemongo.gamepress.gg/optimal-moveset-explanation + See http://pokemongo.gamepress.gg/defensive-tactics + """ + + # Prepare movesets + movesets = [] + types = poke_info['types'] + for fm in poke_info['Fast Attack(s)']: + for chm in poke_info['Special Attack(s)']: + movesets.append(Moveset(fm, chm, types, pokemon_id)) + assert len(movesets) > 0 + + # Calculate attack perfection for each moveset + movesets = sorted(movesets, key=lambda m: m.dps_attack) + worst_dps = movesets[0].dps_attack + best_dps = movesets[-1].dps_attack + if best_dps > worst_dps: + for moveset in movesets: + current_dps = moveset.dps_attack + moveset.attack_perfection = \ + (current_dps - worst_dps) / (best_dps - worst_dps) + + # Calculate defense perfection for each moveset + movesets = sorted(movesets, key=lambda m: m.dps_defense) + worst_dps = movesets[0].dps_defense + best_dps = movesets[-1].dps_defense + if best_dps > worst_dps: + for moveset in movesets: + current_dps = moveset.dps_defense + moveset.defense_perfection = \ + (current_dps - worst_dps) / (best_dps - worst_dps) + + return sorted(movesets, key=lambda m: m.dps, reverse=True) + + @classmethod + def _process_attacks(cls, poke_info, charged=False): + # type: (dict, bool) -> List[Attack] + key = 'Fast Attack(s)' if not charged else 'Special Attack(s)' + moves_dict = (ChargedAttacks if charged else FastAttacks).BY_NAME + moves = [] + for name in poke_info[key]: + if name not in moves_dict: + raise KeyError('Unknown {} attack: "{}"'.format( + 'charged' if charged else 'fast', name)) + moves.append(moves_dict[name]) + moves = sorted(moves, key=lambda m: m.dps, reverse=True) + poke_info[key] = moves + assert len(moves) > 0 + return moves @classmethod def data_for(cls, pokemon_id): + # type: (int) -> dict return cls.STATIC_DATA[pokemon_id - 1] @classmethod def name_for(cls, pokemon_id): + # type: (int) -> string return cls.data_for(pokemon_id)['Name'] @classmethod @@ -128,24 +240,194 @@ def first_evolution_id_for(cls, pokemon_id): return pokemon_id @classmethod - def next_evolution_id_for(cls, pokemon_id): + def prev_evolution_id_for(cls, pokemon_id): + data = cls.data_for(pokemon_id) + if 'Previous evolution(s)' in data: + return int(data['Previous evolution(s)'][-1]['Number']) + return None + + @classmethod + def next_evolution_ids_for(cls, pokemon_id): try: - return int(cls.data_for(pokemon_id)['Next evolution(s)'][0]['Number']) + next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] except KeyError: - return None + return [] + # get only next level evolutions, not all possible + ids = [] + for p in next_evolutions: + p_id = int(p['Number']) + if cls.prev_evolution_id_for(p_id) == pokemon_id: + ids.append(p_id) + return ids @classmethod - def evolution_cost_for(cls, pokemon_id): + def last_evolution_ids_for(cls, pokemon_id): try: - return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] except KeyError: - return + return [pokemon_id] + # get only final evolutions, not all possible + ids = [] + for p in next_evolutions: + p_id = int(p['Number']) + if len(cls.data_for(p_id).get('Next evolution(s)', [])) == 0: + ids.append(p_id) + assert len(ids) > 0 + return ids + + @classmethod + def has_next_evolution(cls, pokemon_id): + poke_info = cls.data_for(pokemon_id) + return 'Next Evolution Requirements' in poke_info \ + or 'Next evolution(s)' in poke_info + + @classmethod + def evolution_cost_for(cls, pokemon_id): + if not cls.has_next_evolution(pokemon_id): + return None + return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + + def parse(self, item): + if 'is_egg' in item: + return Egg(item) + return Pokemon(item) 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)] + +# +# Static Components + +class LevelToCPm(_StaticInventoryComponent): + """ + Data for the CP multipliers at different levels + See http://pokemongo.gamepress.gg/cp-multiplier + See https://github.com/justinleewells/pogo-optimizer/blob/edd692d/data/game/level-to-cpm.json + """ + + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'level_to_cpm.json') + MAX_LEVEL = 40 + MAX_CPM = .0 + # half of the lowest difference between CPMs + HALF_DIFF_BETWEEN_HALF_LVL = 14e-3 + + @classmethod + def init_static_data(cls): + super(LevelToCPm, cls).init_static_data() + cls.MAX_CPM = cls.cp_multiplier_for(cls.MAX_LEVEL) + + @classmethod + def cp_multiplier_for(cls, level): + # type: (Union[float, int, string]) -> float + level = float(level) + level = str(int(level) if level.is_integer() else level) + return cls.STATIC_DATA[level] + + @classmethod + def level_from_cpm(cls, cp_multiplier): + # type: (float) -> float + for lvl, cpm in cls.STATIC_DATA.iteritems(): + diff = abs(cpm - cp_multiplier) + if diff <= cls.HALF_DIFF_BETWEEN_HALF_LVL: + return float(lvl) + raise ValueError("Unknown cp_multiplier: {}".format(cp_multiplier)) + + +class _Attacks(_StaticInventoryComponent): + BY_NAME = {} # type: Dict[string, Attack] + BY_TYPE = {} # type: Dict[List[Attack]] + BY_DPS = [] # type: List[Attack] + + @classmethod + def process_static_data(cls, moves): + ret = {} + by_type = {} + by_name = {} + fast = cls is FastAttacks + for attack in moves: + attack = Attack(attack) if fast else ChargedAttack(attack) + ret[attack.id] = attack + by_name[attack.name] = attack + + if attack.type not in by_type: + by_type[attack.type] = [] + by_type[attack.type].append(attack) + + for t in by_type.iterkeys(): + attacks = sorted(by_type[t], key=lambda m: m.dps, reverse=True) + min_dps = attacks[-1].dps + max_dps = attacks[0].dps - min_dps + if max_dps > .0: + for attack in attacks: # type: Attack + attack.rate_in_type = (attack.dps - min_dps) / max_dps + by_type[t] = attacks + + cls.BY_NAME = by_name + cls.BY_TYPE = by_type + cls.BY_DPS = sorted(ret.values(), key=lambda m: m.dps, reverse=True) + + return ret + + @classmethod + def data_for(cls, attack_id): + # type: (int) -> Attack + if attack_id not in cls.STATIC_DATA: + raise ValueError("Attack {} not found in {}".format( + attack_id, cls.__name__)) + return cls.STATIC_DATA[attack_id] + + @classmethod + def by_name(cls, name): + # type: (string) -> Attack + return cls.BY_NAME[name] + + @classmethod + def list_for_type(cls, type_name): + # type: (string) -> List[Attack] + """ + :return: Attacks sorted by DPS in descending order + """ + return cls.BY_TYPE[type_name] + + @classmethod + def all(cls): + return cls.STATIC_DATA.values() + + @classmethod + def all_by_dps(cls): + return cls.BY_DPS + + +class FastAttacks(_Attacks): + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'fast_moves.json') + + +class ChargedAttacks(_Attacks): + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'charged_moves.json') + + +# +# Instances + +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 Egg(object): def __init__(self, data): self._data = data @@ -157,50 +439,306 @@ def has_next_evolution(self): class Pokemon(object): def __init__(self, data): self._data = data + # Unique ID for this particular Pokemon self.id = data['id'] + # Id of the such pokemons in pokedex self.pokemon_id = data['pokemon_id'] + + # Combat points value self.cp = data['cp'] + # Base CP multiplier, fixed at the catch time + self.cp_bm = data['cp_multiplier'] + # Changeable part of the CP multiplier, increasing at power up + self.cp_am = data.get('additional_cp_multiplier', .0) + # Resulting CP multiplier + self.cp_m = self.cp_bm + self.cp_am + + # Current pokemon level (half of level is a normal value) + self.level = LevelToCPm.level_from_cpm(self.cp_m) + + # Maximum health points + self.hp_max = data['stamina_max'] + # Current health points + self.hp = data.get('stamina', self.hp_max) + assert 0 <= self.hp <= self.hp_max + + # Individial Values of the current pokemon (different for each pokemon) + self.iv_attack = data.get('individual_attack', 0) + self.iv_defense = data.get('individual_defense', 0) + self.iv_stamina = data.get('individual_stamina', 0) + self._static_data = Pokemons.data_for(self.pokemon_id) self.name = Pokemons.name_for(self.pokemon_id) - self.iv = self._compute_iv() + self.nickname = data.get('nickname', self.name) + + self.in_fort = 'deployed_fort_id' in data + self.is_favorite = data.get('favorite', 0) is 1 + + # Basic Values of the current pokemon (identical for all such pokemons) + self.base_attack = self._static_data['BaseAttack'] + self.base_defense = self._static_data['BaseDefense'] + self.base_stamina = self._static_data['BaseStamina'] + + # Maximum possible CP for the current pokemon + self.max_cp = self._static_data['max_cp'] + + self.fast_attack = FastAttacks.data_for(data['move_1']) + self.charged_attack = ChargedAttacks.data_for(data['move_2']) # type: ChargedAttack + + # Internal values (IV) perfection percent + self.iv = self._compute_iv_perfection() + + # IV CP perfection - kind of IV perfection percent but calculated + # using weight of each IV in its contribution to CP of the best + # evolution of current pokemon + # So it tends to be more accurate than simple IV perfection + self.ivcp = self._compute_cp_perfection() + + # Exact value of current CP (not rounded) + self.cp_exact = _calc_cp( + self.base_attack, self.base_defense, self.base_stamina, + self.iv_attack, self.iv_defense, self.iv_stamina, self.cp_m) + assert max(int(self.cp_exact), 10) == self.cp + + # Percent of maximum possible CP + self.cp_percent = self.cp_exact / self.max_cp + + # Get moveset instance with calculated DPS and perfection percents + self.moveset = self._get_moveset() + + def __str__(self): + return self.name + + def __repr__(self): + return self.name def can_evolve_now(self): - return self.has_next_evolution() and self.candy_quantity > self.evolution_cost + 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 + return Pokemons.has_next_evolution(self.pokemon_id) def has_seen_next_evolution(self): - return pokedex().captured(self.next_evolution_id) + for pokemon_id in self.next_evolution_ids: + if pokedex().captured(pokemon_id): + return True + return False @property - def next_evolution_id(self): - return Pokemons.next_evolution_id_for(self.pokemon_id) + def family_id(self): + return self.first_evolution_id @property def first_evolution_id(self): return Pokemons.first_evolution_id_for(self.pokemon_id) + @property + def prev_evolution_id(self): + return Pokemons.prev_evolution_id_for(self.pokemon_id) + + @property + def next_evolution_ids(self): + return Pokemons.next_evolution_ids_for(self.pokemon_id) + + @property + def last_evolution_ids(self): + return Pokemons.last_evolution_ids_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'] + return Pokemons.evolution_cost_for(self.pokemon_id) + + def _compute_iv_perfection(self): + total_iv = self.iv_attack + self.iv_defense + self.iv_stamina + iv_perfection = round((total_iv / 45.0), 2) + return iv_perfection + + def _compute_cp_perfection(self): + """ + CP perfect percent is more accurate than IV perfect + + We know attack plays an important role in CP, and different + pokemons have different base value, that's means 15/14/15 is + better than 14/15/15 for lot of pokemons, and if one pokemon's + base def is more than base sta, 15/15/14 is better than 15/14/15. + + See https://github.com/jabbink/PokemonGoBot/issues/469 + + So calculate CP perfection at final level for the best of the final + evolutions of the pokemon. + """ + variants = [] + iv_attack = self.iv_attack + iv_defense = self.iv_defense + iv_stamina = self.iv_stamina + cp_m = LevelToCPm.MAX_CPM + last_evolution_ids = self.last_evolution_ids + for pokemon_id in last_evolution_ids: + poke_info = Pokemons.data_for(pokemon_id) + base_attack = poke_info['BaseAttack'] + base_defense = poke_info['BaseDefense'] + base_stamina = poke_info['BaseStamina'] + + # calculate CP variants at maximum level + worst_cp = _calc_cp(base_attack, base_defense, base_stamina, + 0, 0, 0, cp_m) + perfect_cp = _calc_cp(base_attack, base_defense, base_stamina, + cp_multiplier=cp_m) + current_cp = _calc_cp(base_attack, base_defense, base_stamina, + iv_attack, iv_defense, iv_stamina, cp_m) + cp_perfection = (current_cp - worst_cp) / (perfect_cp - worst_cp) + variants.append(cp_perfection) + + # get best value (probably for the best evolution) + cp_perfection = max(variants) + return cp_perfection + + def _get_moveset(self): + move1 = self.fast_attack + move2 = self.charged_attack + movesets = self._static_data['movesets'] + current_moveset = None + for moveset in movesets: # type: Moveset + if moveset.fast_attack == move1 and moveset.charged_attack == move2: + current_moveset = moveset + break + + if current_moveset is None: + error = "Unexpected moveset [{}, {}] for #{} {}," \ + " please update info in pokemon.json and create issue/PR"\ + .format(move1, move2, self.pokemon_id, self.name) + # raise ValueError(error) + logging.getLogger(type(self).__name__).error(error) + current_moveset = Moveset( + move1, move2, self._static_data['types'], self.pokemon_id) + + return current_moveset + + +class Attack(object): + def __init__(self, data): + # self._data = data # Not needed - all saved in fields + self.id = data['id'] + self.name = data['name'] + self.type = data['type'] + self.damage = data['damage'] + self.duration = data['duration'] / 1000.0 # duration in seconds + + # Energy addition for fast attack + # Energy cost for charged attack + self.energy = data['energy'] + + # Damage Per Second + # recalc for better precision + self.dps = self.damage / self.duration - def _compute_iv(self): - total_IV = 0.0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] + # Perfection of the attack in it's type (from 0 to 1) + self.rate_in_type = .0 - 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 + @property + def damage_with_stab(self): + # damage with STAB (Same-type attack bonus) + return self.damage * STAB_FACTOR + + @property + def dps_with_stab(self): + # DPS with STAB (Same-type attack bonus) + return self.dps * STAB_FACTOR + + @property + def energy_per_second(self): + return self.energy / self.duration + + @property + def dodge_window(self): + # TODO: Attack Dodge Window + return NotImplemented + + @property + def is_charged(self): + return False + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + + +class ChargedAttack(Attack): + def __init__(self, data): + super(ChargedAttack, self).__init__(data) + + @property + def is_charged(self): + return True + + +class Moveset(object): + def __init__(self, fm, chm, pokemon_types=(), pokemon_id=-1): + # type: (Attack, ChargedAttack, List[string], int) -> None + if len(pokemon_types) <= 0 < pokemon_id: + pokemon_types = Pokemons.data_for(pokemon_id)['types'] + + self.pokemon_id = pokemon_id + self.fast_attack = fm + self.charged_attack = chm + + # See Pokemons._process_movesets() + # See http://pokemongo.gamepress.gg/optimal-moveset-explanation + # See http://pokemongo.gamepress.gg/defensive-tactics + + fm_number = 100 # for simplicity we use 100 + + fm_energy = fm.energy * fm_number + fm_damage = fm.damage * fm_number + fm_secs = fm.duration * fm_number + + # Defender attacks in intervals of 1 second for the + # first 2 attacks, and then in intervals of 2 seconds + # So add 1.95 seconds to the quick move cool down for defense + # 1.95 is something like an average here + # TODO: Do something better? + fm_defense_secs = (fm.duration + 1.95) * fm_number + + chm_number = fm_energy / chm.energy + chm_damage = chm.damage * chm_number + chm_secs = chm.duration * chm_number + + damage_sum = fm_damage + chm_damage + # raw Damage-Per-Second for the moveset + self.dps = damage_sum / (fm_secs + chm_secs) + # average DPS for defense + self.dps_defense = damage_sum / (fm_defense_secs + chm_secs) + + # apply STAB (Same-type attack bonus) + if fm.type in pokemon_types: + fm_damage *= STAB_FACTOR + if chm.type in pokemon_types: + chm_damage *= STAB_FACTOR + + # DPS for attack (counting STAB) + self.dps_attack = (fm_damage + chm_damage) / (fm_secs + chm_secs) + + # Moveset perfection percent attack and for defense + # Calculated for current pokemon, not between all pokemons + # So 100% perfect moveset can be weak if pokemon is weak (e.g. Caterpie) + self.attack_perfection = .0 + self.defense_perfection = .0 + + # TODO: True DPS for real combat (floor(Attack/200 * MovePower * STAB) + 1) + # See http://pokemongo.gamepress.gg/pokemon-attack-explanation + + def __str__(self): + return '[{}, {}]'.format(self.fast_attack, self.charged_attack) + + def __repr__(self): + return '[{}, {}]'.format(self.fast_attack, self.charged_attack) class Inventory(object): @@ -216,13 +754,55 @@ 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'] + 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) + 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) + +# +# Usage helpers + +# STAB (Same-type attack bonus) +STAB_FACTOR = 1.25 _inventory = None +LevelToCPm() # init LevelToCPm +FastAttacks() # init FastAttacks +ChargedAttacks() # init ChargedAttacks + + +def _calc_cp(base_attack, base_defense, base_stamina, + iv_attack=15, iv_defense=15, iv_stamina=15, + cp_multiplier=LevelToCPm.MAX_CPM): + """ + CP calculation + + CP = (Attack * Defense^0.5 * Stamina^0.5 * CP_Multiplier^2) / 10 + CP = (BaseAtk+AtkIV) * (BaseDef+DefIV)^0.5 * (BaseStam+StamIV)^0.5 * Lvl(CPScalar)^2 / 10 + + See https://www.reddit.com/r/TheSilphRoad/comments/4t7r4d/exact_pokemon_cp_formula/ + See https://www.reddit.com/r/pokemongodev/comments/4t7xb4/exact_cp_formula_from_stats_and_cpm_and_an_update/ + See http://pokemongo.gamepress.gg/pokemon-stats-advanced + See http://pokemongo.gamepress.gg/cp-multiplier + See http://gaming.stackexchange.com/questions/280491/formula-to-calculate-pokemon-go-cp-and-hp + + :param base_attack: Pokemon BaseAttack + :param base_defense: Pokemon BaseDefense + :param base_stamina: Pokemon BaseStamina + :param iv_attack: Pokemon IndividualAttack (0..15) + :param iv_defense: Pokemon IndividualDefense (0..15) + :param iv_stamina: Pokemon IndividualStamina (0..15) + :param cp_multiplier: CP Multiplier (0.79030001 is max - value for level 40) + :return: CP as float + """ + return (base_attack + iv_attack) \ + * ((base_defense + iv_defense)**0.5) \ + * ((base_stamina + iv_stamina)**0.5) \ + * (cp_multiplier ** 2) / 10 + def init_inventory(bot): global _inventory @@ -243,9 +823,23 @@ def candies(refresh=False): return _inventory.candy -def pokemons(): +def pokemons(refresh=False): + if refresh: + refresh_inventory() return _inventory.pokemons def items(): return _inventory.items + + +def levels_to_cpm(): + return LevelToCPm + + +def fast_attacks(): + return FastAttacks + + +def charged_attacks(): + return ChargedAttacks diff --git a/pokemongo_bot/socketio_server/app.py b/pokemongo_bot/socketio_server/app.py index 09c237f910..a970a30479 100644 --- a/pokemongo_bot/socketio_server/app.py +++ b/pokemongo_bot/socketio_server/app.py @@ -20,13 +20,13 @@ def remote_control(sid, command): @sio.on('bot:send_reply') def request_reply(sid, response): event = response.pop('command') - account = response.pop('account') + account = response['account'] event = "{}:{}".format(event, account) sio.emit(event, response) @sio.on('bot:broadcast') def bot_broadcast(sid, env): - event = env.pop('event') - account = env.pop('account') + event = env['event'] + account = env['account'] event_name = "{}:{}".format(event, account) - sio.emit(event_name, data=env['data']) + sio.emit(event_name, data=env) diff --git a/pokemongo_bot/step_walker.py b/pokemongo_bot/step_walker.py index f6c2cbfe96..7727dc6a0c 100644 --- a/pokemongo_bot/step_walker.py +++ b/pokemongo_bot/step_walker.py @@ -55,6 +55,17 @@ def step(self): cLng = self.initLng + scaledDLng + random_lat_long_delta() self.api.set_position(cLat, cLng, 0) + self.bot.event_manager.emit( + 'position_update', + sender=self, + level='debug', + data={ + 'current_position': (cLat, cLng), + 'last_position': (self.initLat, self.initLng), + 'distance': '', + 'distance_unit': '' + } + ) self.bot.heartbeat() sleep(1) # sleep one second plus a random delta diff --git a/pokemongo_bot/tree_config_builder.py b/pokemongo_bot/tree_config_builder.py index 6242747b25..57dc9da33c 100644 --- a/pokemongo_bot/tree_config_builder.py +++ b/pokemongo_bot/tree_config_builder.py @@ -61,7 +61,8 @@ def build(self): ) instance = worker(self.bot, task_config) - workers.append(instance) + if instance.enabled: + workers.append(instance) return workers diff --git a/pokemongo_bot/websocket_remote_control.py b/pokemongo_bot/websocket_remote_control.py index c4e15362b6..cd5bd5af96 100644 --- a/pokemongo_bot/websocket_remote_control.py +++ b/pokemongo_bot/websocket_remote_control.py @@ -42,11 +42,16 @@ def on_remote_command(self, command): command_handler() def get_player_info(self): - player_info = self.bot.get_inventory()['responses']['GET_INVENTORY'] + request = self.bot.api.create_request() + request.get_player() + request.get_inventory() + response_dict = request.call() + inventory = response_dict['responses'].get('GET_INVENTORY', {}) + player_info = response_dict['responses'].get('GET_PLAYER', {}) self.sio.emit( 'bot:send_reply', { - 'result': player_info, + 'result': {'inventory': inventory, 'player': player_info}, 'command': 'get_player_info', 'account': self.bot.config.username } diff --git a/run.bat b/run.bat new file mode 100644 index 0000000000..047c8f6fd4 --- /dev/null +++ b/run.bat @@ -0,0 +1,10 @@ +@echo off +set /a x=0 +:LOOP +echo Running pokecli.py for count: %x% +REM Change the path for python.exe if it's different for you +C:\Python27\python.exe pokecli.py +REM Waits for 60 seconds +ping 127.0.0.1 -n 60 > nul +set /a x+=1 +goto :LOOP diff --git a/run.sh b/run.sh index 0812dfac8c..9938f8c5e0 100755 --- a/run.sh +++ b/run.sh @@ -1,21 +1,22 @@ #!/usr/bin/env bash -pokebotpath=$(pwd) +pokebotpath=$(cd "$(dirname "$0")"; pwd) filename="" if [ ! -z $1 ]; then filename=$1 else filename="./configs/config.json" -if [ ! -f "$filename" ] -then -echo "There's no "$filename" file. use setup.sh -config to creat one." fi + +if [ ! -f "$filename" ]; then +echo "There's no "$filename" file. use setup.sh -config to creat one." fi while true do cd $pokebotpath python pokecli.py -cf $filename -read -p "Press any button or wait 20 seconds." -r -s -n1 -t 20 -echo `date`"Pokebot"$*" Stopped." +echo `date`" Pokebot "$*" Stopped." +read -p "Press any button or wait 20 seconds to continue. +" -r -s -n1 -t 20 done exit 0 diff --git a/setup.sh b/setup.sh index bcff0feefb..5fce474b21 100755 --- a/setup.sh +++ b/setup.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -pokebotpath=$(pwd) -backuppath=$(pwd)"/backup" +pokebotpath=$(cd "$(dirname "$0")"; pwd) +backuppath=$pokebotpath"/backup" function Pokebotupdate () { cd $pokebotpath @@ -26,7 +26,7 @@ rm -rf pgoencrypt function Pokebotconfig () { cd $pokebotpath -read -p "1.google 2.ptc +read -p "enter 1 for google or 2 for ptc " auth read -p "Input username " username @@ -37,7 +37,7 @@ Input location " location read -p "Input gmapkey " gmapkey -cp configs/config.json.example configs/config.json +cp -f configs/config.json.example configs/config.json && chmod 755 if [ "$auth" = "2" ] then sed -i "s/google/ptc/g" configs/config.json @@ -46,7 +46,7 @@ sed -i "s/YOUR_USERNAME/$username/g" configs/config.json sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json sed -i "s/SOME_LOCATION/$location/g" configs/config.json sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json -echo "Edit configs/config.json to modify any other config." +echo "Edit ./configs/config.json to modify any other config." } function Pokebotinstall () { @@ -67,12 +67,14 @@ echo "You are on Mac os" sudo brew update sudo brew install --devel protobuf else -echo "Nothing happend." +echo "Please check if you have python pip protobuf gcc make installed on your device." +echo "Wait 5 seconds to continue or Use ctrl+c to interrupt this shell." +sleep 5 fi sudo pip install virtualenv Pokebotupdate Pokebotencrypt -echo "Install complete." +echo "Install complete. Starting to generate config.json." Pokebotconfig } @@ -89,7 +91,7 @@ echo " -i,--install. Install PokemonGo-Bot." echo " -b,--backup. Backup config files." echo " -c,--config. Easy config generator." echo " -e,--encrypt. Make encrypt.so." -echo " -r,--reset. Force sync dev branch." +echo " -r,--reset. Force sync dev branch." echo " -u,--update. Command git pull to update." } @@ -120,12 +122,13 @@ Pokebothelp ;; *.json) filename=$* +echo "It's better to use run.sh, not this one." cd $pokebotpath if [ ! -f ./configs/"$filename" ] then -echo "There's no ./configs/"$filename" file. It's better to use run.sh not this one." +echo "There's no ./configs/"$filename" file. It's better to use run.sh, not this one." else -Pokebotrun +./run.sh ./configs/"$filename" fi ;; *) diff --git a/tests/inventory_test.py b/tests/inventory_test.py new file mode 100644 index 0000000000..3d5ffd66b6 --- /dev/null +++ b/tests/inventory_test.py @@ -0,0 +1,183 @@ +import unittest + +from pokemongo_bot.inventory import * + + +class InventoryTest(unittest.TestCase): + def test_pokemons(self): + # Init data + self.assertEqual(len(Pokemons().all()), 0) # No inventory loaded here + + obj = Pokemons + self.assertEqual(len(obj.STATIC_DATA), 151) + + for poke_info in obj.STATIC_DATA: + name = poke_info['Name'] + pokemon_id = int(poke_info['Number']) + self.assertTrue(1 <= pokemon_id <= 151) + + self.assertGreaterEqual(len(poke_info['movesets']), 1) + self.assertTrue(262 <= poke_info['max_cp'] <= 4145) + self.assertTrue(1 <= len(poke_info['types']) <= 2) + self.assertTrue(40 <= poke_info['BaseAttack'] <= 284) + self.assertTrue(54 <= poke_info['BaseDefense'] <= 242) + self.assertTrue(20 <= poke_info['BaseStamina'] <= 500) + self.assertTrue(.0 <= poke_info['CaptureRate'] <= .56) + self.assertTrue(.0 <= poke_info['FleeRate'] <= .99) + self.assertTrue(1 <= len(poke_info['Weaknesses']) <= 7) + self.assertTrue(3 <= len(name) <= 10) + + self.assertGreaterEqual(len(poke_info['Classification']), 11) + self.assertGreaterEqual(len(poke_info['Fast Attack(s)']), 1) + self.assertGreaterEqual(len(poke_info['Special Attack(s)']), 1) + + self.assertIs(obj.data_for(pokemon_id), poke_info) + self.assertIs(obj.name_for(pokemon_id), name) + + first_evolution_id = obj.first_evolution_id_for(pokemon_id) + self.assertGreaterEqual(first_evolution_id, 1) + next_evolution_ids = obj.next_evolution_ids_for(pokemon_id) + last_evolution_ids = obj.last_evolution_ids_for(pokemon_id) + candies_cost = obj.evolution_cost_for(pokemon_id) + obj.prev_evolution_id_for(pokemon_id) # just call test + self.assertGreaterEqual(len(last_evolution_ids), 1) + + if not obj.has_next_evolution(pokemon_id): + assert 'Next evolution(s)' not in poke_info + assert 'Next Evolution Requirements' not in poke_info + else: + self.assertGreaterEqual(len(next_evolution_ids), 1) + self.assertLessEqual(len(next_evolution_ids), len(last_evolution_ids)) + + reqs = poke_info['Next Evolution Requirements'] + self.assertEqual(reqs["Family"], first_evolution_id) + candies_name = obj.name_for(first_evolution_id) + ' candies' + self.assertEqual(reqs["Name"], candies_name) + self.assertIsNotNone(candies_cost) + self.assertTrue(12 <= candies_cost <= 400) + self.assertEqual(reqs["Amount"], candies_cost) + + evolutions = poke_info["Next evolution(s)"] + self.assertGreaterEqual(len(evolutions), len(next_evolution_ids)) + + for p in evolutions: + p_id = int(p["Number"]) + self.assertNotEqual(p_id, pokemon_id) + self.assertEqual(p["Name"], obj.name_for(p_id)) + + for p_id in next_evolution_ids: + self.assertEqual(obj.prev_evolution_id_for(p_id), pokemon_id) + prev_evs = obj.data_for(p_id)["Previous evolution(s)"] + self.assertGreaterEqual(len(prev_evs), 1) + self.assertEqual(int(prev_evs[-1]["Number"]), pokemon_id) + self.assertEqual(prev_evs[-1]["Name"], name) + + # Only Eevee has 3 next evolutions + self.assertEqual(len(next_evolution_ids), + 1 if pokemon_id != 133 else 3) + + if "Previous evolution(s)" in poke_info: + for p in poke_info["Previous evolution(s)"]: + p_id = int(p["Number"]) + self.assertNotEqual(p_id, pokemon_id) + self.assertEqual(p["Name"], obj.name_for(p_id)) + + # + # Specific pokemons testing + + poke = Pokemon({ + "num_upgrades": 2, "move_1": 210, "move_2": 69, "pokeball": 2, + "favorite": 1, "pokemon_id": 42, "battles_attacked": 4, + "stamina": 76, "stamina_max": 76, "individual_attack": 9, + "individual_defense": 4, "individual_stamina": 8, + "cp_multiplier": 0.4627983868122101, + "additional_cp_multiplier": 0.018886566162109375, + "cp": 653, "nickname": "Golb", "id": 13632861873471324}) + self.assertEqual(poke.level, 12.5) + self.assertEqual(poke.iv, 0.47) + self.assertAlmostEqual(poke.ivcp, 0.488747515) + self.assertAlmostEqual(poke.max_cp, 1921.34561459) + self.assertAlmostEqual(poke.cp_percent, 0.340368964) + self.assertTrue(poke.is_favorite) + self.assertEqual(poke.name, 'Golbat') + self.assertEqual(poke.nickname, "Golb") + self.assertAlmostEqual(poke.moveset.dps, 10.7540173053) + self.assertAlmostEqual(poke.moveset.dps_attack, 12.14462299) + self.assertAlmostEqual(poke.moveset.dps_defense, 4.876681614) + self.assertAlmostEqual(poke.moveset.attack_perfection, 0.4720730048) + self.assertAlmostEqual(poke.moveset.defense_perfection, 0.8158081497) + + poke = Pokemon({ + "move_1": 221, "move_2": 129, "pokemon_id": 19, "cp": 106, + "individual_attack": 6, "stamina_max": 22, "individual_defense": 14, + "cp_multiplier": 0.37523558735847473, "id": 7841053399}) + self.assertEqual(poke.level, 7.5) + self.assertEqual(poke.iv, 0.44) + self.assertAlmostEqual(poke.ivcp, 0.3804059) + self.assertAlmostEqual(poke.max_cp, 581.64643575) + self.assertAlmostEqual(poke.cp_percent, 0.183759867) + self.assertFalse(poke.is_favorite) + self.assertEqual(poke.name, 'Rattata') + self.assertEqual(poke.nickname, 'Rattata') + self.assertAlmostEqual(poke.moveset.dps, 12.5567813108) + self.assertAlmostEqual(poke.moveset.dps_attack, 15.6959766385) + self.assertAlmostEqual(poke.moveset.dps_defense, 5.54282440561) + self.assertAlmostEqual(poke.moveset.attack_perfection, 0.835172881385) + self.assertAlmostEqual(poke.moveset.defense_perfection, 0.603137650999) + + def test_levels_to_cpm(self): + l2c = LevelToCPm + self.assertIs(levels_to_cpm(), l2c) + max_cpm = l2c.cp_multiplier_for(l2c.MAX_LEVEL) + self.assertEqual(l2c.MAX_LEVEL, 40) + self.assertEqual(l2c.MAX_CPM, max_cpm) + self.assertEqual(len(l2c.STATIC_DATA), 79) + + self.assertEqual(l2c.cp_multiplier_for("1"), 0.094) + self.assertEqual(l2c.cp_multiplier_for(1), 0.094) + self.assertEqual(l2c.cp_multiplier_for(1.0), 0.094) + self.assertEqual(l2c.cp_multiplier_for("17.5"), 0.558830576) + self.assertEqual(l2c.cp_multiplier_for(17.5), 0.558830576) + self.assertEqual(l2c.cp_multiplier_for('40.0'), 0.79030001) + self.assertEqual(l2c.cp_multiplier_for(40.0), 0.79030001) + self.assertEqual(l2c.cp_multiplier_for(40), 0.79030001) + + self.assertEqual(l2c.level_from_cpm(0.79030001), 40.0) + self.assertEqual(l2c.level_from_cpm(0.7903), 40.0) + + def test_attacks(self): + self._test_attacks(fast_attacks, FastAttacks) + self._test_attacks(charged_attacks, ChargedAttacks) + + def _test_attacks(self, callback, clazz): + charged = clazz is ChargedAttacks + self.assertIs(callback(), clazz) + + # check consistency + attacks = clazz.all_by_dps() + number = len(attacks) + self.assertTrue(number > 0) + self.assertGreaterEqual(len(clazz.BY_TYPE), 17) + self.assertEqual(number, len(clazz.all())) + self.assertEqual(number, len(clazz.STATIC_DATA)) + self.assertEqual(number, len(clazz.BY_NAME)) + self.assertEqual(number, sum([len(l) for l in clazz.BY_TYPE.values()])) + + # check data + prev_dps = float("inf") + for attack in attacks: # type: Attack + self.assertGreater(attack.id, 0) + self.assertGreater(len(attack.name), 0) + self.assertGreater(len(attack.type), 0) + self.assertGreaterEqual(attack.damage, 0) + self.assertGreater(attack.duration, .0) + self.assertGreater(attack.energy, 0) + self.assertGreaterEqual(attack.dps, 0) + self.assertTrue(.0 <= attack.rate_in_type <= 1.0) + self.assertLessEqual(attack.dps, prev_dps) + self.assertEqual(attack.is_charged, charged) + self.assertIs(attack, clazz.data_for(attack.id)) + self.assertIs(attack, clazz.by_name(attack.name)) + self.assertTrue(attack in clazz.BY_TYPE[attack.type]) + self.assertIsInstance(attack, ChargedAttack if charged else Attack) + prev_dps = attack.dps diff --git a/tests/tree_config_builder_test.py b/tests/tree_config_builder_test.py index 1992c8187c..f8982cbfad 100644 --- a/tests/tree_config_builder_test.py +++ b/tests/tree_config_builder_test.py @@ -86,6 +86,25 @@ def test_task_with_config(self): tree = builder.build() self.assertTrue(tree[0].config.get('longer_eggs_first', False)) + def test_disabling_task(self): + obj = convert_from_json("""[{ + "type": "HandleSoftBan", + "config": { + "enabled": false + } + }, { + "type": "CatchLuredPokemon", + "config": { + "enabled": true + } + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + tree = builder.build() + + self.assertTrue(len(tree) == 1) + self.assertIsInstance(tree[0], CatchLuredPokemon) + def test_load_plugin_task(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') plugin_loader = PluginLoader() From 8674d0dbbd54cb84c07e1889de012db414329b03 Mon Sep 17 00:00:00 2001 From: Genesis Date: Thu, 11 Aug 2016 09:23:52 +0200 Subject: [PATCH 153/202] UpdateTitleStats -> UpdateLiveStats, new stat, refactoring (#3467) * Renamed UpdateTitleStats to UpdateLiveStats * Cleaned worker documentation * Added documentation for terminal_log and terminal_title * Fixed https://github.com/PokemonGoF/PokemonGo-Bot/pull/3312#issuecomment-238672978 * Made some refactoring * Added captures_per_hour stat that shows estimated pokemon captures per hour * Added a captures_per_hour method in metrics.py * Added unit tests for features added in https://github.com/PokemonGoF/PokemonGo-Bot/pull/3312 * Added unit tests for captures_per_hour * Avoid useless overhead when no output configured * Added default config values in documentation * Fixed issue with title updating on Windows * See https://github.com/PokemonGoF/PokemonGo-Bot/pull/3472 --- pokemongo_bot/cell_workers/__init__.py | 2 +- ...te_title_stats.py => update_live_stats.py} | 109 +++++++++--------- pokemongo_bot/metrics.py | 8 ++ ...tats_test.py => update_live_stats_test.py} | 89 +++++++++----- 4 files changed, 126 insertions(+), 82 deletions(-) rename pokemongo_bot/cell_workers/{update_title_stats.py => update_live_stats.py} (81%) rename tests/{update_title_stats_test.py => update_live_stats_test.py} (58%) diff --git a/pokemongo_bot/cell_workers/__init__.py b/pokemongo_bot/cell_workers/__init__.py index 68d181947a..7933425b63 100644 --- a/pokemongo_bot/cell_workers/__init__.py +++ b/pokemongo_bot/cell_workers/__init__.py @@ -17,4 +17,4 @@ from collect_level_up_reward import CollectLevelUpReward from follow_cluster import FollowCluster from sleep_schedule import SleepSchedule -from update_title_stats import UpdateTitleStats +from update_live_stats import UpdateLiveStats diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_live_stats.py similarity index 81% rename from pokemongo_bot/cell_workers/update_title_stats.py rename to pokemongo_bot/cell_workers/update_live_stats.py index bc40ed82e8..e51253edc5 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_live_stats.py @@ -6,27 +6,17 @@ from pokemongo_bot.worker_result import WorkerResult from pokemongo_bot.tree_config_builder import ConfigException -class UpdateTitleStats(BaseTask): + +class UpdateLiveStats(BaseTask): """ - Periodically updates the terminal title to display stats about the bot. + Periodically displays stats about the bot in the terminal and/or in its title. Fetching some stats requires making API calls. If you're concerned about the amount of calls your bot is making, don't enable this worker. Example config : { - "type": "UpdateTitleStats", - "config": { - "min_interval": 10, - "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"], - } - } - - You can set a logging on terminal mode like this: - - Example logging on console (and disabling title change): - { - "type": "UpdateTitleStats", + "type": "UpdateLiveStats", "config": { "min_interval": 10, "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"], @@ -34,6 +24,15 @@ class UpdateTitleStats(BaseTask): "terminal_title": false } } + + min_interval : The minimum interval at which the stats are displayed, + in seconds (defaults to 120 seconds). + The update interval cannot be accurate as workers run synchronously. + stats : An array of stats to display and their display order (implicitly), + see available stats below (defaults to []). + terminal_log : Logs the stats into the terminal (defaults to false). + terminal_title : Displays the stats into the terminal title (defaults to true). + Available stats : - login : The account login (from the credentials). - username : The trainer name (asked at first in-game connection). @@ -48,6 +47,7 @@ class UpdateTitleStats(BaseTask): - stops_visited : The number of visited stops. - pokemon_encountered : The number of encountered pokemon. - pokemon_caught : The number of caught pokemon. + - captures_per_hour : The estimated number of pokemon captured per hour. - pokemon_released : The number of released pokemon. - pokemon_evolved : The number of evolved pokemon. - pokemon_unseen : The number of pokemon never seen before. @@ -56,16 +56,9 @@ class UpdateTitleStats(BaseTask): - stardust_earned : The number of earned stardust since the bot started. - highest_cp_pokemon : The caught pokemon with the highest CP since the bot started. - most_perfect_pokemon : The most perfect caught pokemon since the bot started. - - min_interval : The minimum interval at which the title is updated, - in seconds (defaults to 10 seconds). - The update interval cannot be accurate as workers run synchronously. - stats : An array of stats to display and their display order (implicitly), - see available stats above. """ SUPPORTED_TASK_API_VERSION = 1 - def __init__(self, bot, config): """ Initializes the worker. @@ -74,62 +67,78 @@ def __init__(self, bot, config): :param config: The task configuration. :type config: dict """ - super(UpdateTitleStats, self).__init__(bot, config) + super(UpdateLiveStats, self).__init__(bot, config) self.next_update = None self.min_interval = int(self.config.get('min_interval', 120)) self.displayed_stats = self.config.get('stats', []) - self.terminal_log = self.config.get('terminal_log', False) - self.terminal_title = self.config.get('terminal_title', True) + self.terminal_log = bool(self.config.get('terminal_log', False)) + self.terminal_title = bool(self.config.get('terminal_title', True)) - self.bot.event_manager.register_event('update_title', parameters=('title',)) - self.bot.event_manager.register_event('log_stats',parameters=('title',)) + self.bot.event_manager.register_event('log_stats', parameters=('stats',)) def initialize(self): pass def work(self): """ - Updates the title if necessary. + Displays the stats if necessary. :return: Always returns WorkerResult.SUCCESS. :rtype: WorkerResult """ if not self._should_display(): return WorkerResult.SUCCESS - title = self._get_stats_title(self._get_player_stats()) - # If title is empty, it couldn't be generated. - if not title: + line = self._get_stats_line(self._get_player_stats()) + # If line is empty, it couldn't be generated. + if not line: return WorkerResult.SUCCESS if self.terminal_title: - self._update_title(title, _platform) + self._update_title(line, _platform) if self.terminal_log: - self._log_on_terminal(title) + self._log_on_terminal(line) return WorkerResult.SUCCESS def _should_display(self): """ - Returns a value indicating whether the title should be updated. - :return: True if the title should be updated; otherwise, False. + Returns a value indicating whether the stats should be displayed. + :return: True if the stats should be displayed; otherwise, False. :rtype: bool """ + if not self.terminal_title and not self.terminal_log: + return False return self.next_update is None or datetime.now() >= self.next_update - def _log_on_terminal(self, title): + def _compute_next_update(self): + """ + Computes the next update datetime based on the minimum update interval. + :return: Nothing. + :rtype: None + """ + self.next_update = datetime.now() + timedelta(seconds=self.min_interval) + + def _log_on_terminal(self, stats): + """ + Logs the stats into the terminal using an event. + :param stats: The stats to display. + :type stats: string + :return: Nothing. + :rtype: None + """ self.emit_event( 'log_stats', - formatted="{title}", + formatted="{stats}", data={ - 'title': title + 'stats': stats } ) - self.next_update = datetime.now() + timedelta(seconds=self.min_interval) + self._compute_next_update() def _update_title(self, title, platform): """ - Updates the window title using different methods, according to the given platform + Updates the window title using different methods, according to the given platform. :param title: The new window title. :type title: string :param platform: The platform string. @@ -139,14 +148,6 @@ def _update_title(self, title, platform): :raise: RuntimeError: When the given platform isn't supported. """ - self.emit_event( - 'update_title', - formatted="{title}", - data={ - 'title': title - } - ) - if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) stdout.flush() @@ -154,14 +155,12 @@ def _update_title(self, title, platform): stdout.write("\033]0;{}\007".format(title)) stdout.flush() elif platform == "win32": - ctypes.windll.kernel32.SetConsoleTitleA(title) + ctypes.windll.kernel32.SetConsoleTitleA(title.encode()) else: raise RuntimeError("unsupported platform '{}'".format(platform)) + self._compute_next_update() - self.next_update = datetime.now() + timedelta(seconds=self.min_interval) - - - def _get_stats_title(self, player_stats): + def _get_stats_line(self, player_stats): """ Generates a stats string with the given player stats according to the configuration. :return: A string containing human-readable stats, ready to be displayed. @@ -194,6 +193,7 @@ def _get_stats_title(self, player_stats): stops_visited = metrics.visits['latest'] - metrics.visits['start'] pokemon_encountered = metrics.num_encounters() pokemon_caught = metrics.num_captures() + captures_per_hour = int(metrics.captures_per_hour()) pokemon_released = metrics.releases pokemon_evolved = metrics.num_evolutions() pokemon_unseen = metrics.num_new_mons() @@ -223,6 +223,7 @@ def _get_stats_title(self, player_stats): 'stops_visited': 'Visited {:,} stops'.format(stops_visited), 'pokemon_encountered': 'Encountered {:,} pokemon'.format(pokemon_encountered), 'pokemon_caught': 'Caught {:,} pokemon'.format(pokemon_caught), + 'captures_per_hour': '{:,} pokemon/h'.format(captures_per_hour), 'pokemon_released': 'Released {:,} pokemon'.format(pokemon_released), 'pokemon_evolved': 'Evolved {:,} pokemon'.format(pokemon_evolved), 'pokemon_unseen': 'Encountered {} new pokemon'.format(pokemon_unseen), @@ -251,9 +252,9 @@ def get_stat(stat): return available_stats[stat] # Map stats the user wants to see to available stats and join them with pipes. - title = ' | '.join(map(get_stat, self.displayed_stats)) + line = ' | '.join(map(get_stat, self.displayed_stats)) - return title + return line def _get_player_stats(self): """ diff --git a/pokemongo_bot/metrics.py b/pokemongo_bot/metrics.py index 0ffeb39a6c..4a6a70cae3 100644 --- a/pokemongo_bot/metrics.py +++ b/pokemongo_bot/metrics.py @@ -42,6 +42,14 @@ def num_throws(self): def num_captures(self): return self.captures['latest'] - self.captures['start'] + def captures_per_hour(self): + """ + Returns an estimated number of pokemon caught per hour. + :return: An estimated number of pokemon caught per hour. + :rtype: float + """ + return self.num_captures() / (time.time() - self.start_time) * 3600 + def num_visits(self): return self.visits['latest'] - self.visits['start'] diff --git a/tests/update_title_stats_test.py b/tests/update_live_stats_test.py similarity index 58% rename from tests/update_title_stats_test.py rename to tests/update_live_stats_test.py index ba480f0151..dc5b140080 100644 --- a/tests/update_title_stats_test.py +++ b/tests/update_live_stats_test.py @@ -2,18 +2,20 @@ from sys import platform as _platform from datetime import datetime, timedelta from mock import call, patch, MagicMock -from pokemongo_bot.cell_workers.update_title_stats import UpdateTitleStats +from pokemongo_bot.cell_workers.update_live_stats import UpdateLiveStats from tests import FakeBot -class UpdateTitleStatsTestCase(unittest.TestCase): +class UpdateLiveStatsTestCase(unittest.TestCase): config = { 'min_interval': 20, 'stats': ['login', 'username', 'pokemon_evolved', 'pokemon_encountered', 'uptime', 'pokemon_caught', 'stops_visited', 'km_walked', 'level', 'stardust_earned', 'level_completion', 'xp_per_hour', 'pokeballs_thrown', 'highest_cp_pokemon', 'level_stats', 'xp_earned', 'pokemon_unseen', 'most_perfect_pokemon', - 'pokemon_stats', 'pokemon_released'] + 'pokemon_stats', 'pokemon_released', 'captures_per_hour'], + 'terminal_log': True, + 'terminal_title': False } player_stats = { 'level': 25, @@ -26,7 +28,7 @@ def setUp(self): self.bot = FakeBot() self.bot._player = {'username': 'Username'} self.bot.config.username = 'Login' - self.worker = UpdateTitleStats(self.bot, self.config) + self.worker = UpdateLiveStats(self.bot, self.config) def mock_metrics(self): self.bot.metrics = MagicMock() @@ -37,6 +39,7 @@ def mock_metrics(self): self.bot.metrics.visits = {'latest': 250, 'start': 30} self.bot.metrics.num_encounters.return_value = 130 self.bot.metrics.num_captures.return_value = 120 + self.bot.metrics.captures_per_hour.return_value = 75 self.bot.metrics.releases = 30 self.bot.metrics.num_evolutions.return_value = 12 self.bot.metrics.num_new_mons.return_value = 3 @@ -45,16 +48,30 @@ def mock_metrics(self): self.bot.metrics.highest_cp = {'desc': 'highest_cp'} self.bot.metrics.most_perfect = {'desc': 'most_perfect'} - def test_process_config(self): + def test_config(self): self.assertEqual(self.worker.min_interval, self.config['min_interval']) self.assertEqual(self.worker.displayed_stats, self.config['stats']) + self.assertEqual(self.worker.terminal_title, self.config['terminal_title']) + self.assertEqual(self.worker.terminal_log, self.config['terminal_log']) def test_should_display_no_next_update(self): self.worker.next_update = None self.assertTrue(self.worker._should_display()) - @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + @patch('pokemongo_bot.cell_workers.update_live_stats.datetime') + def test_should_display_no_terminal_log_title(self, mock_datetime): + # _should_display should return False if both terminal_title and terminal_log are false + # in configuration, even if we're past next_update. + now = datetime.now() + mock_datetime.now.return_value = now + timedelta(seconds=20) + self.worker.next_update = now + self.worker.terminal_log = False + self.worker.terminal_title = False + + self.assertFalse(self.worker._should_display()) + + @patch('pokemongo_bot.cell_workers.update_live_stats.datetime') def test_should_display_before_next_update(self, mock_datetime): now = datetime.now() mock_datetime.now.return_value = now - timedelta(seconds=20) @@ -62,7 +79,7 @@ def test_should_display_before_next_update(self, mock_datetime): self.assertFalse(self.worker._should_display()) - @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + @patch('pokemongo_bot.cell_workers.update_live_stats.datetime') def test_should_display_after_next_update(self, mock_datetime): now = datetime.now() mock_datetime.now.return_value = now + timedelta(seconds=20) @@ -70,7 +87,7 @@ def test_should_display_after_next_update(self, mock_datetime): self.assertTrue(self.worker._should_display()) - @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + @patch('pokemongo_bot.cell_workers.update_live_stats.datetime') def test_should_display_exactly_next_update(self, mock_datetime): now = datetime.now() mock_datetime.now.return_value = now @@ -78,65 +95,83 @@ def test_should_display_exactly_next_update(self, mock_datetime): self.assertTrue(self.worker._should_display()) - @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') - def test_next_update_after_update_title(self, mock_datetime): + @patch('pokemongo_bot.cell_workers.update_live_stats.datetime') + def test_compute_next_update(self, mock_datetime): now = datetime.now() mock_datetime.now.return_value = now old_next_display_value = self.worker.next_update - self.worker._update_title('', 'linux2') + self.worker._compute_next_update() self.assertNotEqual(self.worker.next_update, old_next_display_value) self.assertEqual(self.worker.next_update, now + timedelta(seconds=self.config['min_interval'])) - @patch('pokemongo_bot.cell_workers.update_title_stats.stdout') - def test_update_title_linux_cygwin(self, mock_stdout): + @patch('pokemongo_bot.cell_workers.update_live_stats.stdout') + @patch('pokemongo_bot.cell_workers.UpdateLiveStats._compute_next_update') + def test_update_title_linux_cygwin(self, mock_compute_next_update, mock_stdout): self.worker._update_title('new title linux', 'linux') self.assertEqual(mock_stdout.write.call_count, 1) self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title linux\x07')) + self.assertEqual(mock_compute_next_update.call_count, 1) self.worker._update_title('new title linux2', 'linux2') self.assertEqual(mock_stdout.write.call_count, 2) self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title linux2\x07')) + self.assertEqual(mock_compute_next_update.call_count, 2) self.worker._update_title('new title cygwin', 'cygwin') self.assertEqual(mock_stdout.write.call_count, 3) self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title cygwin\x07')) + self.assertEqual(mock_compute_next_update.call_count, 3) - @patch('pokemongo_bot.cell_workers.update_title_stats.stdout') - def test_update_title_darwin(self, mock_stdout): + @patch('pokemongo_bot.cell_workers.update_live_stats.stdout') + @patch('pokemongo_bot.cell_workers.UpdateLiveStats._compute_next_update') + def test_update_title_darwin(self, mock_compute_next_update, mock_stdout): self.worker._update_title('new title darwin', 'darwin') self.assertEqual(mock_stdout.write.call_count, 1) self.assertEqual(mock_stdout.write.call_args, call('\033]0;new title darwin\007')) + self.assertEqual(mock_compute_next_update.call_count, 1) @unittest.skipUnless(_platform.startswith("win"), "requires Windows") - @patch('pokemongo_bot.cell_workers.update_title_stats.ctypes') - def test_update_title_win32(self, mock_ctypes): + @patch('pokemongo_bot.cell_workers.update_live_stats.ctypes') + @patch('pokemongo_bot.cell_workers.UpdateLiveStats._compute_next_update') + def test_update_title_win32(self, mock_compute_next_update, mock_ctypes): self.worker._update_title('new title win32', 'win32') self.assertEqual(mock_ctypes.windll.kernel32.SetConsoleTitleA.call_count, 1) self.assertEqual(mock_ctypes.windll.kernel32.SetConsoleTitleA.call_args, call('new title win32')) + self.assertEqual(mock_compute_next_update.call_count, 1) + + @patch('pokemongo_bot.cell_workers.update_live_stats.BaseTask.emit_event') + @patch('pokemongo_bot.cell_workers.UpdateLiveStats._compute_next_update') + def test_log_on_terminal(self, mock_compute_next_update, mock_emit_event): + self.worker._log_on_terminal('stats') + + self.assertEqual(mock_emit_event.call_count, 1) + self.assertEqual(mock_emit_event.call_args, + call('log_stats', data={'stats': 'stats'}, formatted='{stats}')) + self.assertEqual(mock_compute_next_update.call_count, 1) - def test_get_stats_title_player_stats_none(self): - title = self.worker._get_stats_title(None) + def test_get_stats_line_player_stats_none(self): + line = self.worker._get_stats_line(None) - self.assertEqual(title, '') + self.assertEqual(line, '') - def test_get_stats_no_displayed_stats(self): + def test_get_stats_line_no_displayed_stats(self): self.worker.displayed_stats = [] - title = self.worker._get_stats_title(self.player_stats) + line = self.worker._get_stats_line(self.player_stats) - self.assertEqual(title, '') + self.assertEqual(line, '') - def test_get_stats(self): + def test_get_stats_line(self): self.mock_metrics() - title = self.worker._get_stats_title(self.player_stats) + line = self.worker._get_stats_line(self.player_stats) expected = 'Login | Username | Evolved 12 pokemon | Encountered 130 pokemon | ' \ 'Uptime : 15:42:13 | Caught 120 pokemon | Visited 220 stops | ' \ '42.05km walked | Level 25 | Earned 24,069 Stardust | ' \ @@ -145,6 +180,6 @@ def test_get_stats(self): '+424,242 XP | Encountered 3 new pokemon | ' \ 'Most perfect pokemon : most_perfect | ' \ 'Encountered 130 pokemon, 120 caught, 30 released, 12 evolved, ' \ - '3 never seen before | Released 30 pokemon' + '3 never seen before | Released 30 pokemon | 75 pokemon/h' - self.assertEqual(title, expected) + self.assertEqual(line, expected) From 045f297563fb28eaf17fc047d4d3d438aced2d94 Mon Sep 17 00:00:00 2001 From: nivong Date: Thu, 11 Aug 2016 09:29:10 +0200 Subject: [PATCH 154/202] Update installation docs to reflect setup sh changes (#3567) --- docs/installation.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/installation.md b/docs/installation.md index 8f5f5692e3..6c515dfee2 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -7,6 +7,18 @@ - [docker](https://docs.docker.com/engine/installation/) (Optional) - [how to setup after installation](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/How-to-run-with-Docker) - [protobuf 3](https://github.com/google/protobuf) (OS Dependent, see below) +#Linux/Mac Automatic installation +### Easy installation (automatic, linux only) +1. Run setup.sh -e +2. Run setup.sh -i +3. Run setup.sh -c + +### To update +1. Run setup.sh -r +2. Run setup.sh -u + + +# Manual installation ### Protobuf 3 installation - OS X: `brew update && brew install --devel protobuf` From 350148e8443e35a6f5b74169b7ac5fd26815fe7b Mon Sep 17 00:00:00 2001 From: nivong Date: Thu, 11 Aug 2016 09:29:57 +0200 Subject: [PATCH 155/202] Fixed chmod in setup.sh (#3565) * forgot to include the config location * fixed setup files --- CONTRIBUTORS.md | 1 + setup.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index fc2478f20c..136886b5aa 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -62,3 +62,4 @@ * eevee-github * g0vanish * cmezh + * Nivong diff --git a/setup.sh b/setup.sh index 5fce474b21..81c8d13419 100755 --- a/setup.sh +++ b/setup.sh @@ -37,7 +37,7 @@ Input location " location read -p "Input gmapkey " gmapkey -cp -f configs/config.json.example configs/config.json && chmod 755 +cp -f configs/config.json.example configs/config.json && chmod 755 configs/config.json if [ "$auth" = "2" ] then sed -i "s/google/ptc/g" configs/config.json From de09f34ebb292d303ba44066fde842f293546594 Mon Sep 17 00:00:00 2001 From: nivong Date: Thu, 11 Aug 2016 10:07:44 +0200 Subject: [PATCH 156/202] Updated readme to have better readability (#3569) * Made the READ me more read friendly and compactor. * Update README.md * Update README.md * Update README.md * Update README.md * Update faq.md --- README.md | 32 +++++++------------------------- docs/faq.md | 9 +++++++-- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index d372870ca6..d2172efbb2 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,14 @@ - -# PokemonGo-Bot (Working) +# PokemonGo-Bot PokemonGo bot is a project created by the [PokemonGoF](https://github.com/PokemonGoF) team. -The project is currently setup in two main branches. `dev` and `master`. - -## Help Needed on [Desktop Version](https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) -## Please submit PR to [Dev branch](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) +The project is currently setup in two main branches. `dev` also known as `beta` and `master` also known as `stable`. Submit your PR's to `dev`. -We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) -You can count on the community in #help channel. +If you need any help please don't create an issue here on github we have a great community on Slack, [Click here to join the chat!](https://pokemongo-bot.herokuapp.com). You can count on the community in #help channel. ## Table of Contents +- [Installation] (https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/installation.md) - [Features](#features) -- [Wiki](#wiki) +- [Wiki](https://github.com/PokemonGoF/PokemonGo-Bot/wiki) - [Credits](#credits) ## Features @@ -27,9 +23,9 @@ You can count on the community in #help channel. - [x] Rudimentary IV Functionality filter - [x] Ignore certain pokemon filter - [x] Adjust delay between Pokemon capture & Transfer as per configuration -- [ ] Standalone Desktop Application - [x] Hatch eggs - [x] Incubate eggs +- [ ] [Standalone Desktop Application] (https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) - [ ] Use candy - [ ] Inventory cleaner @@ -45,22 +41,8 @@ If there are any concerns with this policy or you believe we are tracking someth If you do not want any data to be gathered, you can turn off this feature by setting `health_record` to `false` in your `config.json`. -## Wiki -All information on [Getting Started](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Getting-Started) is available in the [Wiki](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/)! -- __Installation__ - - [Requirements] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#requirements-click-each-one-for-install-guide) - - [How to run with Docker](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/How-to-run-with-Docker) - - [Linux] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-linux) - - [Mac] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-mac) - - [Windows] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-windows) -- [Develop PokemonGo-Bot](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Develop-PokemonGo-Bot) -- [Plugins](https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/plugins.md) -- [Configuration-files](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files) -- [Front end web module - Google Maps API] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page)) -- [Docker Usage](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ#how-to-run-with-docker) -- [FAQ](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ) +## Help Needed on [Desktop Version](https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) -To ensure that all updates are documented - [@eggins](https://github.com/eggins) will keep the Wiki updated with the latest information on installing, updating and configuring the bot. ## Credits - [tejado](https://github.com/tejado) many thanks for the API diff --git a/docs/faq.md b/docs/faq.md index 7720f0a64c..ba2524bbce 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,7 +1,12 @@ ### How do I start the application? -After customizing your config.json files, cd to the PokemonGo-Bot folder and enter: +After [installing] (https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/installation.md), in the root folder run the following command: +### Linux ``` -$ python pokecli.py +run.sh +``` +### Windows +``` +run.bat ``` This will start the application. From 962313328db8c6509abef217b6e98dbaf4c3b3fb Mon Sep 17 00:00:00 2001 From: nivong Date: Thu, 11 Aug 2016 10:26:47 +0200 Subject: [PATCH 157/202] More documentation changes making it noob proof(er) (#3575) * added information on what what does * add a link to the docker hub for NAS users. --- docs/docker.md | 10 +++++++++- docs/installation.md | 14 +++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/docs/docker.md b/docs/docker.md index 04823a2f5c..735cdd99ec 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,4 +1,12 @@ -Start by downloading for your platform: [Mac](https://www.docker.com/products/docker#/mac), [Windows](https://www.docker.com/products/docker#/windows), or [Linux](https://www.docker.com/products/docker#/linux). Once you have Docker installed, simply create the various config.json files for your different accounts (e.g. `configs/config-account1.json`) and then create a Docker image for PokemonGo-Bot using the Dockerfile in this repo. +Start by downloading for your platform: [Mac](https://www.docker.com/products/docker#/mac), [Windows](https://www.docker.com/products/docker#/windows), or [Linux](https://www.docker.com/products/docker#/linux). Once you have Docker installed, simply create the various config.json files for your different accounts (e.g. `configs/config-account1.json`) and then create a Docker image for PokemonGo-Bot using the Dockerfile in this [repo](https://hub.docker.com/r/svlentink/pokemongo-bot/). + +#Automatic setup +Use this docker hub url: https://hub.docker.com/r/svlentink/pokemongo-bot/ +``` +docker pull svlentink/pokemongo-bot +``` + +#Manual setup ``` cd PokemonGo-Bot docker build -t pokemongo-bot . diff --git a/docs/installation.md b/docs/installation.md index 6c515dfee2..d469fa85f5 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -8,14 +8,22 @@ - [protobuf 3](https://github.com/google/protobuf) (OS Dependent, see below) #Linux/Mac Automatic installation -### Easy installation (automatic, linux only) +### Easy installation 1. Run setup.sh -e + This will create the needed encrypted.so file 2. Run setup.sh -i + This will install the bot and all stuff that is needed to run it (follow the guide) 3. Run setup.sh -c + This will make the config file needed, only basic stuff is changed here. If you want to edit more edit this file after: config/config.json +4. Run run.sh + This will run the bot and will start leveling your pokemon go account. ### To update -1. Run setup.sh -r -2. Run setup.sh -u +1. Stop the bot if it's running. (use control + c twice to stop it) +2. Run setup.sh -r + This will reset and makes sure you have no changes made to any code since it will overide it +3. Run setup.sh -u + This will run git pull and will update to the new git update. # Manual installation From 59fea6c888a913bef8048da90600f4659c7648d4 Mon Sep 17 00:00:00 2001 From: nivong Date: Thu, 11 Aug 2016 10:31:26 +0200 Subject: [PATCH 158/202] Update Readme to point to the latest wiki and documentation (#3579) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d2172efbb2..1c0fa41910 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ The project is currently setup in two main branches. `dev` also known as `beta` If you need any help please don't create an issue here on github we have a great community on Slack, [Click here to join the chat!](https://pokemongo-bot.herokuapp.com). You can count on the community in #help channel. ## Table of Contents -- [Installation] (https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/installation.md) +- [Installation](https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/installation.md) +- [Documentation](https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/) - [Features](#features) -- [Wiki](https://github.com/PokemonGoF/PokemonGo-Bot/wiki) - [Credits](#credits) ## Features From d5812e9053de1ac52f6487969e1a616148e4889b Mon Sep 17 00:00:00 2001 From: Yong Wen Chua Date: Thu, 11 Aug 2016 17:10:51 +0800 Subject: [PATCH 159/202] Improve Docker Image and `docker-compose.yml` (#3583) - Use the non-`onbuild` variant of the python base image to better make use of Docker image caching - Update some volumes in `docker-compose.yml` for pogoweb --- Dockerfile | 12 +++++++++--- docker-compose.yml | 9 +++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index f98d5d6942..2520813273 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,7 @@ -FROM python:2.7-onbuild +FROM python:2.7 + +WORKDIR /usr/src/app +VOLUME ["/usr/app/configs", "/usr/src/app/web"] ARG timezone=Etc/UTC RUN echo $timezone > /etc/timezone \ @@ -15,8 +18,11 @@ RUN cd /tmp && wget "http://pgoapi.com/pgoencrypt.tar.gz" \ && cd /tmp \ && rm -rf /tmp/pgoencrypt* -VOLUME ["/usr/src/app/web"] - ENV LD_LIBRARY_PATH /usr/src/app +COPY requirements.txt /usr/src/app/ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . /usr/src/app + ENTRYPOINT ["python", "pokecli.py"] diff --git a/docker-compose.yml b/docker-compose.yml index 95a32f8ceb..d31830d401 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,8 @@ services: bot1-pokego: build: . volumes: - - ./configs/config.json:/usr/src/app/configs/config.json + - ./configs:/usr/src/app/configs + - ./web:/usr/src/app/web stdin_open: true tty: true bot1-pokegoweb: @@ -11,10 +12,10 @@ services: ports: - "8000:8000" volumes_from: - - bot1-pokego + - bot1-pokego volumes: - - ./configs/userdata.js:/usr/src/app/web/config/userdata.js + - ./configs:/usr/src/app/web/config working_dir: /usr/src/app/web command: bash -c "echo 'Serving HTTP on 0.0.0.0 port 8000' && python -m SimpleHTTPServer > /dev/null 2>&1" depends_on: - - bot1-pokego \ No newline at end of file + - bot1-pokego From 651c909c697784ef7344eea997d78613d54d141f Mon Sep 17 00:00:00 2001 From: Konstantin Shapkin Date: Thu, 11 Aug 2016 12:53:57 +0300 Subject: [PATCH 160/202] Small fixes and improvments in setup.sh (#3585) Adding -p to mkdir - it will not show "already exist folder" error. And adding to backup *gpx and path files. --- CONTRIBUTORS.md | 1 + setup.sh | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 136886b5aa..7374cfa94d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -63,3 +63,4 @@ * g0vanish * cmezh * Nivong + * kestel diff --git a/setup.sh b/setup.sh index 81c8d13419..c5acdf9c8b 100755 --- a/setup.sh +++ b/setup.sh @@ -109,8 +109,10 @@ Pokebotreset Pokebotupdate ;; --backup|-b) -mkdir $backuppath +mkdir -p $backuppath cp -f $pokebotpath/configs/config*.json $backuppath/ +cp -f $pokebotpath/configs/*.gpx $backuppath/ +cp -f $pokebotpath/configs/path*.json $backuppath/ cp -f $pokebotpath/web/config/userdata.js $backuppath/ echo "Backup complete" ;; From 0292c2f426b2432be70b0ca867fa62d8436cb5bf Mon Sep 17 00:00:00 2001 From: achretien Date: Thu, 11 Aug 2016 12:36:05 +0200 Subject: [PATCH 161/202] Remove the "evolve_captured" flag in favor of the EvolveTask (#3530) * Remove the "evolve_captured" flag in favor of the EvolveTask * Remove unused event * Warn the user instead of stopping the bot --- configs/config.json.cluster.example | 1 - configs/config.json.example | 1 - configs/config.json.map.example | 1 - configs/config.json.path.example | 1 - configs/config.json.pokemon.example | 1 - pokecli.py | 20 +------ pokemongo_bot/__init__.py | 4 -- .../cell_workers/pokemon_catch_worker.py | 54 ------------------- .../event_handlers/colored_logging_handler.py | 1 - 9 files changed, 2 insertions(+), 82 deletions(-) diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index eb507cd43c..73ba037703 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -78,7 +78,6 @@ "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, - "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, "logging_color": true, diff --git a/configs/config.json.example b/configs/config.json.example index 59974ba156..1594a18f5e 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -93,7 +93,6 @@ "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, - "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, "logging_color": true, diff --git a/configs/config.json.map.example b/configs/config.json.map.example index cf6604976b..f20fb9f1ac 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -320,7 +320,6 @@ "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, - "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, "logging_color": true, diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 6b6619573b..08867f1967 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -80,7 +80,6 @@ "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, - "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, "logging_color": true, diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 1dfa01199d..3216ad6681 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -86,7 +86,6 @@ "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, - "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, "logging_color": true, diff --git a/pokecli.py b/pokecli.py index f59ae3acb9..4544ccf124 100644 --- a/pokecli.py +++ b/pokecli.py @@ -326,15 +326,6 @@ def _json_loader(filename): type=str, default='km' ) - add_config( - parser, - load, - short_flag="-ec", - long_flag="--evolve_captured", - help="(Ad-hoc mode) Pass \"all\" or a list of pokemon to evolve (e.g., \"Pidgey,Weedle,Caterpie\"). Bot will attempt to evolve all the pokemon captured!", - type=str, - default=[] - ) add_config( parser, load, @@ -453,12 +444,8 @@ def task_configuration_error(flag_name): task_configuration_error('{}.{}'.format(outer, inner)) return None - if (config.evolve_captured - and (not isinstance(config.evolve_captured, str) - or str(config.evolve_captured).lower() in ["true", "false"])): - parser.error('"evolve_captured" should be list of pokemons: use "all" or "none" to match all ' + - 'or none of the pokemons, or use a comma separated list such as "Pidgey,Weedle,Caterpie"') - return None + if "evolve_captured" in load: + logger.warning('The evolve_captured argument is no longer supported. Please use the EvolvePokemon task instead') if not (config.location or config.location_cache): parser.error("Needs either --use-location-cache or --location.") @@ -483,9 +470,6 @@ def task_configuration_error(flag_name): if not os.path.isdir(web_dir): raise - if config.evolve_captured and isinstance(config.evolve_captured, str): - config.evolve_captured = [str(pokemon_name).strip() for pokemon_name in config.evolve_captured.split(',')] - fix_nested_config(config) return config diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 1b18ac7ff8..5d10cfba02 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -301,10 +301,6 @@ def _register_events(self): 'pokemon_evolved', parameters=('pokemon', 'iv', 'cp') ) - self.event_manager.register_event( - 'pokemon_evolve_fail', - parameters=('pokemon',) - ) 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') diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 3b2092e535..77e29ae10b 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -191,30 +191,6 @@ def _is_vip_pokemon(self, pokemon): return True return self._pokemon_matches_config(self.config.vips, pokemon, default_logic='or') - def _get_current_pokemon_ids(self): - # don't use cached bot.get_inventory() here because we need to have actual information in capture logic - response_dict = self.api.get_inventory() - - try: - inventory_items = response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] - except KeyError: - return [] # no items - - id_list = [] - for item in inventory_items: - try: - pokemon = item['inventory_item_data']['pokemon_data'] - except KeyError: - continue - - # ignore eggs - if pokemon.get('is_egg'): - continue - - id_list.append(pokemon['id']) - - return id_list - def _pct(self, rate_by_ball): return '{0:.2f}'.format(rate_by_ball * 100) @@ -326,9 +302,6 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) berry_count -= 1 - # get current pokemon list before catch - pokemon_before_catch = self._get_current_pokemon_ids() - # try to catch pokemon! items_stock[current_ball] -= 1 self.emit_event( @@ -418,31 +391,4 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): self.bot.softban = False - # evolve pokemon if necessary - if self.config.evolve_captured and (self.config.evolve_captured[0] == 'all' or pokemon.name in self.config.evolve_captured): - pokemon_after_catch = self._get_current_pokemon_ids() - pokemon_to_evolve = list(set(pokemon_after_catch) - set(pokemon_before_catch)) - - if len(pokemon_to_evolve) == 0: - break - - self._do_evolve(pokemon, pokemon_to_evolve[0]) - break - - def _do_evolve(self, pokemon, new_pokemon_id): - response_dict = self.api.evolve_pokemon(pokemon_id=new_pokemon_id) - catch_pokemon_status = response_dict['responses']['EVOLVE_POKEMON']['result'] - - if catch_pokemon_status == 1: - self.emit_event( - 'pokemon_evolved', - formatted='{pokemon} evolved!', - data={'pokemon': pokemon.name} - ) - else: - self.emit_event( - 'pokemon_evolve_fail', - formatted='Failed to evolve {pokemon}!', - data={'pokemon': pokemon.name} - ) diff --git a/pokemongo_bot/event_handlers/colored_logging_handler.py b/pokemongo_bot/event_handlers/colored_logging_handler.py index f3c902d464..6c527511ba 100644 --- a/pokemongo_bot/event_handlers/colored_logging_handler.py +++ b/pokemongo_bot/event_handlers/colored_logging_handler.py @@ -71,7 +71,6 @@ class ColoredLoggingHandler(EventHandler): 'moving_to_fort': 'white', 'moving_to_lured_fort': 'white', 'pokemon_catch_rate': 'white', - 'pokemon_evolve_fail': 'white', 'pokestop_on_cooldown': 'white', 'pokestop_out_of_range': 'white', 'polyline_request': 'white', From 138b66413e45efcef1ae015388b80a2725e7a7d8 Mon Sep 17 00:00:00 2001 From: Oscar Fanelli Date: Thu, 11 Aug 2016 14:38:46 +0200 Subject: [PATCH 162/202] Improved documentation (#3604) - Improved read flow - Added volume sharing of cache folder --- docs/docker.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/docker.md b/docs/docker.md index 735cdd99ec..2b2181bb56 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,25 +1,34 @@ Start by downloading for your platform: [Mac](https://www.docker.com/products/docker#/mac), [Windows](https://www.docker.com/products/docker#/windows), or [Linux](https://www.docker.com/products/docker#/linux). Once you have Docker installed, simply create the various config.json files for your different accounts (e.g. `configs/config-account1.json`) and then create a Docker image for PokemonGo-Bot using the Dockerfile in this [repo](https://hub.docker.com/r/svlentink/pokemongo-bot/). -#Automatic setup +#Setup +##Automatic setup Use this docker hub url: https://hub.docker.com/r/svlentink/pokemongo-bot/ ``` docker pull svlentink/pokemongo-bot ``` -#Manual setup +##Manual setup ``` cd PokemonGo-Bot docker build -t pokemongo-bot . ``` + +#Run + You can verify that the image was created with: ``` docker images ``` +- In case of automatic setup, you'll see an image called: `svlentink/pokemongo-bot` +- In case of manual setup, you'll see an image called: `pokemongo-bot` To run PokemonGo-Bot Docker image you've created, simple run: ``` -docker run --name=pokego-bot1 --rm -it -v $(pwd)/configs/config-account1.json:/usr/src/app/configs/config.json pokemongo-bot +docker run --name=pokego-bot1 --rm -it -v $(pwd)/configs/config-account1.json:/usr/src/app/configs/config.json -v $(pwd)/data:/usr/src/app/data pokemongo-bot ``` + +Replace `pokemongo-bot` with `svlentink/pokemongo-bot` in case you followed automatic setup. + _Check the logs in real-time `docker logs -f pgobot`_ If you want to run multiple accounts with the same Docker image, simply specify different config.json and names in the Docker run command. From 66336308792a6e36bbd297e5e9b42eb98d6ff334 Mon Sep 17 00:00:00 2001 From: devn0ll Date: Thu, 11 Aug 2016 19:34:21 +0200 Subject: [PATCH 163/202] Update installation.md (#3618) * Update installation.md Changed the linux Section and the Ubuntu example, for easyer access (if anyone now how to make a code block collapsable feel free) * Update installation.md removed pastebin link (cause i put it there in the first place) because it's not needed edittet linux section to Installation Linux on the example of Ubuntu (merged it with ubuntu install section) i hope i didn't miss anything will check on mac and windows in the next 24h. I will make a new pr for that and link it here --- docs/installation.md | 75 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 12 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index d469fa85f5..873638ff54 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -49,20 +49,71 @@ To update your project do (in the project folder): `git pull` To update python requirement packages do (in the project folder): `pip install --upgrade -r requirements.txt` -### Installation Linux -(change master to dev for the latest version) - -``` -$ git clone --recursive -b master https://github.com/PokemonGoF/PokemonGo-Bot -$ cd PokemonGo-Bot -$ virtualenv . -$ source bin/activate -$ pip install -r requirements.txt -``` -#### Example Installation for Ubuntu +### Linux Installation +####on the Example of Ubuntu (change dev to master for the lastest master version) -http://pastebin.com/pzPjXT65 +if you are on a different Linux OS you maybe have to adapt things like: + +- package mananger (for example yum instead of apt-get) +- package names + +```bash +##install +#change to root +sudo -i +#go to your home directory with the console +apt-get install build-essential autoconf libtool pkg-config make python-dev python-protobuf python2.7 wget git +#install pip +wget https://bootstrap.pypa.io/get-pip.py +python2.7 get-pip.py +rm -f get-pip.py +#get git repo +git clone --recursive -b dev https://github.com/PokemonGoF/PokemonGo-Bot +cd PokemonGo-Bot +#install and enable virtualenv +#You need to make shure your python version and virtualenv verison work together +#install virtualenv and activate it +pip install virtualenv +virtualenv . +source bin/activate +#then install the requierements +pip install -r requirements.txt + +##get the encryption.so and move to right folder +wget http://pgoapi.com/pgoencrypt.tar.gz +tar -xzvf pgoencrypt.tar.gz +cd pgoencrypt/src/ +make +cd ../../ +#make the encrypt able to load +mv pgoencrypt/src/libencrypt.so encrypt.so + +##edit the configuration file +cp configs/config.json.example configs/config.json +vi configs/config.json +# gedit is possible too with 'gedit configs/config.json' +#edit "google" to "ptc" if you have a pokemon trainer account +#edit all settings + + +##update to newest +#if you need to do more i'll update this file +#make shure virtualenv is enabled and you are in the correct folder +git pull +pip install -r requirements.txt + +##start the bot +./run.sh configs/config.json + +##after reboot or closing the terminal +#at every new start go into the folder of the PokemonGo-Bot by +#going into the folder where you startet installing it an then +cd PokemonGo-Bot +#activate virtualenv and start +source bin/activate +./run.sh configs/config.json +``` ### Installation Mac From f3c0ce3fc85ebd722c393724585191dc54c1f6d6 Mon Sep 17 00:00:00 2001 From: Sander Date: Thu, 11 Aug 2016 19:34:50 +0200 Subject: [PATCH 164/202] moving_to_fort and moving_to_lured_fort now also emit current_position (#3614) arrived_at_fort emits position --- pokemongo_bot/__init__.py | 13 ++++++++++--- pokemongo_bot/cell_workers/move_to_fort.py | 7 ++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 5d10cfba02..f3c9a3bd53 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -183,7 +183,8 @@ def _register_events(self): 'moving_to_fort', parameters=( 'fort_name', - 'distance' + 'distance', + 'current_position' ) ) self.event_manager.register_event( @@ -191,7 +192,8 @@ def _register_events(self): parameters=( 'fort_name', 'distance', - 'lure_distance' + 'lure_distance', + 'current_position' ) ) self.event_manager.register_event( @@ -217,7 +219,12 @@ def _register_events(self): parameters=('status_code',) ) self.event_manager.register_event('pokestop_searching_too_often') - self.event_manager.register_event('arrived_at_fort') + self.event_manager.register_event( + 'arrived_at_fort', + parameters=( + 'current_position' + ) + ) # pokemon stuff self.event_manager.register_event( diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 24ecf5e74a..33dd5cf576 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -61,6 +61,7 @@ def work(self): fort_event_data = { 'fort_name': u"{}".format(fort_name), 'distance': format_dist(dist, unit), + 'current_position': self.bot.position } if self.is_attracted() > 0: @@ -87,9 +88,13 @@ def work(self): if not step_walker.step(): return WorkerResult.RUNNING + arrived_at_fort_data = { + 'current_position': self.bot.position + } self.emit_event( 'arrived_at_fort', - formatted='Arrived at fort.' + formatted='Arrived at fort.', + data=arrived_at_fort_data ) return WorkerResult.SUCCESS From 78649ae81b2201250f0aa48157ae556712f39bde Mon Sep 17 00:00:00 2001 From: joaodragao Date: Thu, 11 Aug 2016 18:40:15 +0100 Subject: [PATCH 165/202] Fix handle soft ban (#3629) * Fix `MoveToFort.config` None to Empty {} dict Whenever the bot ticks, `config.get('enable', True)` is required * Add new contributor --- CONTRIBUTORS.md | 1 + pokemongo_bot/cell_workers/handle_soft_ban.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 7374cfa94d..7606647a3c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -64,3 +64,4 @@ * cmezh * Nivong * kestel + * joaodragao diff --git a/pokemongo_bot/cell_workers/handle_soft_ban.py b/pokemongo_bot/cell_workers/handle_soft_ban.py index 8018b7c33e..cf88eeac83 100644 --- a/pokemongo_bot/cell_workers/handle_soft_ban.py +++ b/pokemongo_bot/cell_workers/handle_soft_ban.py @@ -29,7 +29,7 @@ def work(self): ) if fort_distance > Constants.MAX_DISTANCE_FORT_IS_REACHABLE: - MoveToFort(self.bot, config=None).work() + MoveToFort(self.bot, config={}).work() self.bot.recent_forts = self.bot.recent_forts[0:-1] if forts[0]['id'] in self.bot.fort_timeouts: del self.bot.fort_timeouts[forts[0]['id']] From 1bfabca76050742eefdbca79d9ba1279737c6219 Mon Sep 17 00:00:00 2001 From: mmns Date: Thu, 11 Aug 2016 19:58:46 +0200 Subject: [PATCH 166/202] [config] new tasks in example files (#3457) * add UpdateTitleStats to config examples * add UpdateTitleStats to config examples * add SleepSchedule to config examples all config files are missing SleepSchedule from task see https://github.com/PokemonGoF/PokemonGo-Bot/pokemongo_bot/cell_workers/sleep_schedule.py for more info , { "type": "SleepSchedule", "config": { "time": "22:54", "duration":"7:46", "time_random_offset": "00:24", "duration_random_offset": "00:43" } }, * Forgot one * fix for changes in #3467 * disable new tasks by default, removed duplicate --- configs/config.json.cluster.example | 20 ++++++++++++++++++++ configs/config.json.example | 20 ++++++++++++++++++++ configs/config.json.map.example | 20 ++++++++++++++++++++ configs/config.json.path.example | 20 ++++++++++++++++++++ configs/config.json.pokemon.example | 20 ++++++++++++++++++++ 5 files changed, 100 insertions(+) diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index 73ba037703..bb01bb44f3 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -9,6 +9,16 @@ { "type": "HandleSoftBan" }, + { + "type": "SleepSchedule", + "config": { + "enabled": false, + "time": "22:54", + "duration":"7:46", + "time_random_offset": "00:24", + "duration_random_offset": "00:43" + } + }, { "type": "CollectLevelUpReward" }, @@ -18,6 +28,16 @@ "longer_eggs_first": true } }, + { + "type": "UpdateLiveStats", + "config": { + "enabled": false, + "min_interval": 10, + "stats": ["uptime", "stardust_earned", "xp_earned", "xp_per_hour", "stops_visited"], + "terminal_log": true, + "terminal_title": true + } + }, { "type": "TransferPokemon" }, diff --git a/configs/config.json.example b/configs/config.json.example index 1594a18f5e..d2f28cf18e 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -9,6 +9,16 @@ { "type": "HandleSoftBan" }, + { + "type": "SleepSchedule", + "config": { + "enabled": false, + "time": "22:54", + "duration":"7:46", + "time_random_offset": "00:24", + "duration_random_offset": "00:43" + } + }, { "type": "CollectLevelUpReward" }, @@ -18,6 +28,16 @@ "longer_eggs_first": true } }, + { + "type": "UpdateLiveStats", + "config": { + "enabled": false, + "min_interval": 10, + "stats": ["uptime", "stardust_earned", "xp_earned", "xp_per_hour", "stops_visited"], + "terminal_log": true, + "terminal_title": true + } + }, { "type": "TransferPokemon" }, diff --git a/configs/config.json.map.example b/configs/config.json.map.example index f20fb9f1ac..acb6b2f5ea 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -9,6 +9,16 @@ { "type": "HandleSoftBan" }, + { + "type": "SleepSchedule", + "config": { + "enabled": false, + "time": "22:54", + "duration":"7:46", + "time_random_offset": "00:24", + "duration_random_offset": "00:43" + } + }, { "type": "CollectLevelUpReward" }, @@ -18,6 +28,16 @@ "longer_eggs_first": true } }, + { + "type": "UpdateLiveStats", + "config": { + "enabled": false, + "min_interval": 10, + "stats": ["uptime", "stardust_earned", "xp_earned", "xp_per_hour", "stops_visited"], + "terminal_log": true, + "terminal_title": true + } + }, { "type": "TransferPokemon" }, diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 08867f1967..52286b2809 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -9,6 +9,16 @@ { "type": "HandleSoftBan" }, + { + "type": "SleepSchedule", + "config": { + "enabled": false, + "time": "22:54", + "duration":"7:46", + "time_random_offset": "00:24", + "duration_random_offset": "00:43" + } + }, { "type": "CollectLevelUpReward" }, @@ -18,6 +28,16 @@ "longer_eggs_first": true } }, + { + "type": "UpdateLiveStats", + "config": { + "enabled": false, + "min_interval": 10, + "stats": ["uptime", "stardust_earned", "xp_earned", "xp_per_hour", "stops_visited"], + "terminal_log": true, + "terminal_title": true + } + }, { "type": "TransferPokemon" }, diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 3216ad6681..c271f21fcc 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -9,6 +9,16 @@ { "type": "HandleSoftBan" }, + { + "type": "SleepSchedule", + "config": { + "enabled": false, + "time": "22:54", + "duration":"7:46", + "time_random_offset": "00:24", + "duration_random_offset": "00:43" + } + }, { "type": "CollectLevelUpReward" }, @@ -18,6 +28,16 @@ "longer_eggs_first": true } }, + { + "type": "UpdateLiveStats", + "config": { + "enabled": false, + "min_interval": 10, + "stats": ["uptime", "stardust_earned", "xp_earned", "xp_per_hour", "stops_visited"], + "terminal_log": true, + "terminal_title": true + } + }, { "type": "TransferPokemon" }, From 01ac445d48b5a5adeda84ed58388c525c6314f4a Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Thu, 11 Aug 2016 21:39:34 +0200 Subject: [PATCH 167/202] Revert "moving_to_fort and moving_to_lured_fort now also emit current_position" (#3640) --- pokemongo_bot/__init__.py | 13 +++---------- pokemongo_bot/cell_workers/move_to_fort.py | 7 +------ 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index f3c9a3bd53..5d10cfba02 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -183,8 +183,7 @@ def _register_events(self): 'moving_to_fort', parameters=( 'fort_name', - 'distance', - 'current_position' + 'distance' ) ) self.event_manager.register_event( @@ -192,8 +191,7 @@ def _register_events(self): parameters=( 'fort_name', 'distance', - 'lure_distance', - 'current_position' + 'lure_distance' ) ) self.event_manager.register_event( @@ -219,12 +217,7 @@ def _register_events(self): parameters=('status_code',) ) self.event_manager.register_event('pokestop_searching_too_often') - self.event_manager.register_event( - 'arrived_at_fort', - parameters=( - 'current_position' - ) - ) + self.event_manager.register_event('arrived_at_fort') # pokemon stuff self.event_manager.register_event( diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 33dd5cf576..24ecf5e74a 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -61,7 +61,6 @@ def work(self): fort_event_data = { 'fort_name': u"{}".format(fort_name), 'distance': format_dist(dist, unit), - 'current_position': self.bot.position } if self.is_attracted() > 0: @@ -88,13 +87,9 @@ def work(self): if not step_walker.step(): return WorkerResult.RUNNING - arrived_at_fort_data = { - 'current_position': self.bot.position - } self.emit_event( 'arrived_at_fort', - formatted='Arrived at fort.', - data=arrived_at_fort_data + formatted='Arrived at fort.' ) return WorkerResult.SUCCESS From 543226e81b864d0fd714337e7cce1da8ac0a0005 Mon Sep 17 00:00:00 2001 From: Eli White Date: Thu, 11 Aug 2016 13:17:51 -0700 Subject: [PATCH 168/202] Return RUNNING if there are more forts to spin (#3412) --- pokemongo_bot/cell_workers/spin_fort.py | 33 +++++++++++-------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 9422d8ec35..61d3eb02bd 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -27,11 +27,13 @@ def should_run(self): return self.ignore_item_count or self.bot.has_space_for_loot() def work(self): - fort = self.get_fort_in_range() + forts = self.get_forts_in_range() - if not self.should_run() or fort is None: + if not self.should_run() or len(forts) == 0: return WorkerResult.SUCCESS + fort = forts[0] + lat = fort['latitude'] lng = fort['longitude'] @@ -135,11 +137,15 @@ def work(self): ) else: self.bot.fort_timeouts[fort["id"]] = (time.time() + 300) * 1000 # Don't spin for 5m - return 11 + return WorkerResult.ERROR sleep(2) - return 0 - def get_fort_in_range(self): + if len(forts) > 1: + return WorkerResult.RUNNING + + return WorkerResult.SUCCESS + + def get_forts_in_range(self): forts = self.bot.get_forts(order_by_distance=True) for fort in forts: @@ -147,21 +153,12 @@ def get_fort_in_range(self): self.bot.fort_timeouts[fort["id"]] = fort['cooldown_complete_timestamp_ms'] forts.remove(fort) - forts = filter(lambda x: x["id"] not in self.bot.fort_timeouts, forts) - - if len(forts) == 0: - return None - - fort = forts[0] - - distance_to_fort = distance( + forts = filter(lambda fort: fort["id"] not in self.bot.fort_timeouts, forts) + forts = filter(lambda fort: distance( self.bot.position[0], self.bot.position[1], fort['latitude'], fort['longitude'] - ) - - if distance_to_fort <= Constants.MAX_DISTANCE_FORT_IS_REACHABLE: - return fort + ) <= Constants.MAX_DISTANCE_FORT_IS_REACHABLE, forts) - return None + return forts From b033783bc76a849f486ad3e912243f498343bc46 Mon Sep 17 00:00:00 2001 From: extink Date: Thu, 11 Aug 2016 22:44:25 +0200 Subject: [PATCH 169/202] Correct colored logging (#3637) * Rewrite colered logging handler to use python logging * Add all terminal colors and a format reset * Add name to contributors list --- CONTRIBUTORS.md | 1 + .../event_handlers/colored_logging_handler.py | 243 +++++++----------- 2 files changed, 96 insertions(+), 148 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 7606647a3c..725360784b 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -65,3 +65,4 @@ * Nivong * kestel * joaodragao + * extink diff --git a/pokemongo_bot/event_handlers/colored_logging_handler.py b/pokemongo_bot/event_handlers/colored_logging_handler.py index 6c527511ba..cedd0e53dc 100644 --- a/pokemongo_bot/event_handlers/colored_logging_handler.py +++ b/pokemongo_bot/event_handlers/colored_logging_handler.py @@ -1,87 +1,88 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import time -import sys -import struct + +import logging from pokemongo_bot.event_manager import EventHandler + class ColoredLoggingHandler(EventHandler): EVENT_COLOR_MAP = { - 'api_error': 'red', - 'bot_exit': 'red', - 'bot_start': 'green', - 'config_error': 'red', - 'egg_already_incubating': 'yellow', - 'egg_hatched': 'green', - 'future_pokemon_release': 'yellow', - 'incubate': 'green', - 'incubator_already_used': 'yellow', - 'inventory_full': 'yellow', - 'item_discard_fail': 'red', - 'item_discarded': 'green', - 'keep_best_release': 'green', - 'level_up': 'green', - 'level_up_reward': 'green', - 'location_cache_error': 'yellow', - 'location_cache_ignored': 'yellow', - 'login_failed': 'red', - 'login_successful': 'green', - 'lucky_egg_error': 'red', - 'move_to_map_pokemon_encounter': 'green', - 'move_to_map_pokemon_fail': 'red', - 'next_egg_incubates': 'yellow', - 'next_sleep': 'green', - 'no_pokeballs': 'red', - 'pokemon_appeared': 'yellow', - 'pokemon_capture_failed': 'red', - 'pokemon_caught': 'blue', - 'pokemon_evolved': 'green', - 'pokemon_fled': 'red', - 'pokemon_inventory_full': 'red', - 'pokemon_nickname_invalid': 'red', - 'pokemon_not_in_range': 'yellow', - 'pokemon_release': 'green', - 'pokemon_vanished': 'red', - 'pokestop_empty': 'yellow', - 'pokestop_searching_too_often': 'yellow', - 'rename_pokemon': 'green', - 'skip_evolve': 'yellow', - 'softban': 'red', - 'spun_pokestop': 'cyan', - 'threw_berry_failed': 'red', - 'unknown_spin_result': 'red', - 'unset_pokemon_nickname': 'red', - 'vip_pokemon': 'red', + 'api_error': 'red', + 'bot_exit': 'red', + 'bot_start': 'green', + 'config_error': 'red', + 'egg_already_incubating': 'yellow', + 'egg_hatched': 'green', + 'future_pokemon_release': 'yellow', + 'incubate': 'green', + 'incubator_already_used': 'yellow', + 'inventory_full': 'yellow', + 'item_discard_fail': 'red', + 'item_discarded': 'green', + 'keep_best_release': 'green', + 'level_up': 'green', + 'level_up_reward': 'green', + 'location_cache_error': 'yellow', + 'location_cache_ignored': 'yellow', + 'login_failed': 'red', + 'login_successful': 'green', + 'lucky_egg_error': 'red', + 'move_to_map_pokemon_encounter': 'green', + 'move_to_map_pokemon_fail': 'red', + 'next_egg_incubates': 'yellow', + 'next_sleep': 'green', + 'no_pokeballs': 'red', + 'pokemon_appeared': 'yellow', + 'pokemon_capture_failed': 'red', + 'pokemon_caught': 'blue', + 'pokemon_evolved': 'green', + 'pokemon_fled': 'red', + 'pokemon_inventory_full': 'red', + 'pokemon_nickname_invalid': 'red', + 'pokemon_not_in_range': 'yellow', + 'pokemon_release': 'green', + 'pokemon_vanished': 'red', + 'pokestop_empty': 'yellow', + 'pokestop_searching_too_often': 'yellow', + 'rename_pokemon': 'green', + 'skip_evolve': 'yellow', + 'softban': 'red', + 'spun_pokestop': 'cyan', + 'threw_berry_failed': 'red', + 'unknown_spin_result': 'red', + 'unset_pokemon_nickname': 'red', + 'vip_pokemon': 'red', # event names for 'white' still here to remember that these events are already determined its color. - 'arrived_at_cluster': 'white', - 'arrived_at_fort': 'white', - 'bot_sleep': 'white', - 'catchable_pokemon': 'white', - 'found_cluster': 'white', - 'incubate_try': 'white', - 'load_cached_location': 'white', - 'location_found': 'white', - 'login_started': 'white', - 'lured_pokemon_found': 'white', - 'move_to_map_pokemon_move_towards': 'white', - 'move_to_map_pokemon_teleport_back': 'white', - 'move_to_map_pokemon_updated_map': 'white', - 'moving_to_fort': 'white', - 'moving_to_lured_fort': 'white', - 'pokemon_catch_rate': 'white', - 'pokestop_on_cooldown': 'white', - 'pokestop_out_of_range': 'white', - 'polyline_request': 'white', - 'position_update': 'white', - 'set_start_location': 'white', - 'softban_fix': 'white', - 'softban_fix_done': 'white', - 'spun_fort': 'white', - 'threw_berry': 'white', - 'threw_pokeball': 'white', - 'used_lucky_egg': 'white' + 'arrived_at_cluster': 'white', + 'arrived_at_fort': 'white', + 'bot_sleep': 'white', + 'catchable_pokemon': 'white', + 'found_cluster': 'white', + 'incubate_try': 'white', + 'load_cached_location': 'white', + 'location_found': 'white', + 'login_started': 'white', + 'lured_pokemon_found': 'white', + 'move_to_map_pokemon_move_towards': 'white', + 'move_to_map_pokemon_teleport_back': 'white', + 'move_to_map_pokemon_updated_map': 'white', + 'moving_to_fort': 'white', + 'moving_to_lured_fort': 'white', + 'pokemon_catch_rate': 'white', + 'pokemon_evolve_fail': 'white', + 'pokestop_on_cooldown': 'white', + 'pokestop_out_of_range': 'white', + 'polyline_request': 'white', + 'position_update': 'white', + 'set_start_location': 'white', + 'softban_fix': 'white', + 'softban_fix_done': 'white', + 'spun_fort': 'white', + 'threw_berry': 'white', + 'threw_pokeball': 'white', + 'used_lucky_egg': 'white' } CONTINUOUS_EVENT_NAMES = [ 'catchable_pokemon', @@ -89,83 +90,29 @@ class ColoredLoggingHandler(EventHandler): 'spun_fort' ] COLOR_CODE = { - 'red': '91', - 'green': '92', - 'yellow': '93', - 'blue': '94', - 'cyan': '96' + 'gray': '\033[90m', + 'red': '\033[91m', + 'green': '\033[92m', + 'yellow': '\033[93m', + 'blue': '\033[94m', + 'magenta': '\033[95m', + 'cyan': '\033[96m', + 'white': '\033[97m', + 'reset': '\033[0m' } - def __init__(self): - self._last_event = None - try: - # this `try ... except` is for ImportError on Windows - import fcntl - import termios - self._ioctl = fcntl.ioctl - self._TIOCGWINSZ = termios.TIOCGWINSZ - except ImportError: - self._ioctl = None - self._TIOCGWINSZ = None - def handle_event(self, event, sender, level, formatted_msg, data): - # Prepare message string - message = None - if formatted_msg: - try: - message = formatted_msg.decode('utf-8') - except UnicodeEncodeError: - message = formatted_msg - else: - message = '{}'.format(str(data)) - - # Replace message if necessary - if event == 'catchable_pokemon': - message = 'Something rustles nearby!' + logger = logging.getLogger(type(sender).__name__) - # Truncate previous line if same event continues - if event in ColoredLoggingHandler.CONTINUOUS_EVENT_NAMES and self._last_event == event and sys.stdout.isatty(): - # Filling with "' ' * terminal_width" in order to completely clear last line - terminal_width = self._terminal_width() - if terminal_width: - sys.stdout.write('\r{}\r'.format(' ' * terminal_width)) - else: - sys.stdout.write('\r') - else: - sys.stdout.write("\n") - - color_name = None - if event in ColoredLoggingHandler.EVENT_COLOR_MAP: - color_name = ColoredLoggingHandler.EVENT_COLOR_MAP[event] - - # Change color if necessary + color = self.COLOR_CODE['white'] + if event in self.EVENT_COLOR_MAP: + color = self.COLOR_CODE[self.EVENT_COLOR_MAP[event]] if event == 'egg_hatched' and data.get('pokemon', 'error') == 'error': - # `egg_hatched` event will be dispatched in both cases: hatched pokemon info is successfully taken or not. - # change color from 'green' to 'red' in case of error. - color_name = 'red' + color = self.COLOR_CODE['red'] + formatted_msg = '{}{}{}'.format(color, formatted_msg, self.COLOR_CODE['reset']) - if color_name in ColoredLoggingHandler.COLOR_CODE: - sys.stdout.write( - '[{time}] \033[{color}m{message}\033[0m'.format( - time=time.strftime("%H:%M:%S"), - color=ColoredLoggingHandler.COLOR_CODE[color_name], - message=message - ) - ) + if formatted_msg: + message = "[{}] {}".format(event, formatted_msg) else: - sys.stdout.write('[{time}] {message}'.format( - time=time.strftime("%H:%M:%S"), - message=message - )) - - sys.stdout.flush() - self._last_event = event - - def _terminal_width(self): - if self._ioctl is None or self._TIOCGWINSZ is None: - return None - - h, w, hp, wp = struct.unpack(str('HHHH'), - self._ioctl(0, self._TIOCGWINSZ, - struct.pack(str('HHHH'), 0, 0, 0, 0))) - return w + message = '{}: {}'.format(event, str(data)) + getattr(logger, level)(message) From 44d7b39ae3446ddc1af083be6d8dae38d4c68326 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Thu, 11 Aug 2016 22:23:28 +0100 Subject: [PATCH 170/202] Allow to set throw quality and spin odds (#2534) * Allow to set throw quality and spin odds * Throw Parameter : add example * Throw parameter example : update indentation --- configs/config.json.example | 7 ++ pokecli.py | 45 ++++++++++++ .../cell_workers/pokemon_catch_worker.py | 68 +++++++++++++++++-- pokemongo_bot/human_behaviour.py | 19 ------ 4 files changed, 113 insertions(+), 26 deletions(-) diff --git a/configs/config.json.example b/configs/config.json.example index d2f28cf18e..ae1bdb02fe 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -121,6 +121,13 @@ "// Example of always catching Rattata:": {}, "// Rattata": { "always_catch" : true } }, + "catch_throw_parameters": { + "excellent_rate": 0.1, + "great_rate": 0.5, + "nice_rate": 0.3, + "normal_rate": 0.1, + "spin_success_rate" : 0.6, + }, "release": { "any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}, "// Example of always releasing Rattata:": {}, diff --git a/pokecli.py b/pokecli.py index 4544ccf124..2cd5553ba5 100644 --- a/pokecli.py +++ b/pokecli.py @@ -394,6 +394,51 @@ def _json_loader(filename): type=bool, default=True ) + add_config( + parser, + load, + short_flag="-cte", + long_flag="--catch_throw_parameters.excellent_rate", + help="Define the odd of performing an excellent throw", + type=float, + default=1 + ) + add_config( + parser, + load, + short_flag="-ctg", + long_flag="--catch_throw_parameters.great_rate", + help="Define the odd of performing a great throw", + type=float, + 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 config = parser.parse_args() diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 77e29ae10b..451ca0e9df 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- import time +from random import random from pokemongo_bot import inventory from pokemongo_bot.base_task import BaseTask -from pokemongo_bot.human_behaviour import normalized_reticle_size, sleep, spin_modifier +from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.worker_result import WorkerResult CATCH_STATUS_SUCCESS = 1 @@ -69,6 +70,7 @@ def work(self, response_dict=None): # validate response if not response_dict: return WorkerResult.ERROR + try: responses = response_dict['responses'] response = responses[self.response_key] @@ -301,8 +303,18 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and berry_count > 0 and not used_berry: catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) berry_count -= 1 + + # Randomize the quality of the throw + # Default structure + throw_parameters = {'normalized_reticle_size': 1.950, + 'spin_modifier': 1.0, + 'normalized_hit_position': 1.0, + 'throw_type_label': 'Excellent'} + self.generate_spin_parameter(throw_parameters) + self.generate_throw_quality_parameters(throw_parameters) # try to catch pokemon! + # TODO : Log which type of throw we selected items_stock[current_ball] -= 1 self.emit_event( 'threw_pokeball', @@ -314,17 +326,14 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): } ) - reticle_size_parameter = normalized_reticle_size(self.config.catch_randomize_reticle_factor) - spin_modifier_parameter = spin_modifier(self.config.catch_randomize_spin_factor) - response_dict = self.api.catch_pokemon( encounter_id=encounter_id, pokeball=current_ball, - normalized_reticle_size=reticle_size_parameter, + normalized_reticle_size=throw_parameters['normalized_reticle_size'], spawn_point_id=self.spawn_point_guid, hit_pokemon=1, - spin_modifier=spin_modifier_parameter, - normalized_hit_position=1 + spin_modifier=throw_parameters['spin_modifier'], + normalized_hit_position=throw_parameters['normalized_hit_position'] ) try: @@ -392,3 +401,48 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): self.bot.softban = False break + + def generate_spin_parameter(self, throw_parameters): + spin_success_rate = self.config.catch_throw_parameters_spin_success_rate + if random() <= spin_success_rate: + throw_parameters['spin_modifier'] = 0.5 + 0.5 * random() + else: + throw_parameters['spin_modifier'] = 0.499 * random() + + def generate_throw_quality_parameters(self, throw_parameters): + throw_excellent_chance = self.config.catch_throw_parameters_excellent_rate + throw_great_chance = self.config.catch_throw_parameters_great_rate + throw_nice_chance = self.config.catch_throw_parameters_nice_rate + throw_normal_throw_chance = self.config.catch_throw_parameters_normal_rate + + # 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/human_behaviour.py b/pokemongo_bot/human_behaviour.py index 2a8d2d5e9f..37a95081ca 100644 --- a/pokemongo_bot/human_behaviour.py +++ b/pokemongo_bot/human_behaviour.py @@ -25,22 +25,3 @@ def random_lat_long_delta(): # 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) From c344fe294e6c4baa08403d961f0f3fb5b4f4749b Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Thu, 11 Aug 2016 23:40:01 +0200 Subject: [PATCH 171/202] sending location update if distance to move is smaller than step size --- pokemongo_bot/step_walker.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pokemongo_bot/step_walker.py b/pokemongo_bot/step_walker.py index 7727dc6a0c..97a4f14b47 100644 --- a/pokemongo_bot/step_walker.py +++ b/pokemongo_bot/step_walker.py @@ -39,6 +39,17 @@ def __init__(self, bot, speed, dest_lat, dest_lng): def step(self): if (self.dLat == 0 and self.dLng == 0) or self.dist < self.speed: self.api.set_position(self.destLat, self.destLng, 0) + self.bot.event_manager.emit( + 'position_update', + sender=self, + level='debug', + data={ + 'current_position': (self.destLat, self.destLng), + 'last_position': (self.initLat, self.initLng), + 'distance': '', + 'distance_unit': '' + } + ) self.bot.heartbeat() return True From c1517fdd36c46f358ae90277e16c527d0d42df71 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Fri, 12 Aug 2016 00:03:27 +0200 Subject: [PATCH 172/202] removing useless comma --- configs/config.json.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configs/config.json.example b/configs/config.json.example index ae1bdb02fe..de2436807a 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -126,7 +126,7 @@ "great_rate": 0.5, "nice_rate": 0.3, "normal_rate": 0.1, - "spin_success_rate" : 0.6, + "spin_success_rate" : 0.6 }, "release": { "any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}, From ca4a1977a80d544fa3afd331d32fd60399f1f621 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Fri, 12 Aug 2016 00:04:02 +0200 Subject: [PATCH 173/202] removing old catch randomizer stuff --- configs/config.json.example | 2 -- 1 file changed, 2 deletions(-) diff --git a/configs/config.json.example b/configs/config.json.example index de2436807a..502c18c78e 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -113,8 +113,6 @@ "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, - "catch_randomize_reticle_factor": 1.0, - "catch_randomize_spin_factor": 1.0, "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, From b8af467f8e694adf3e2353876e27159d4f16daee Mon Sep 17 00:00:00 2001 From: Brice Date: Fri, 12 Aug 2016 00:08:15 +0200 Subject: [PATCH 174/202] 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 + } + ) From f83e76777ae13bde1e4ee2470c6c1b3f31ebb7ac Mon Sep 17 00:00:00 2001 From: hongxu Date: Fri, 12 Aug 2016 10:34:47 +0800 Subject: [PATCH 175/202] remove Debian python-protobuf dependency (#3670) - during installation of pgoapi, protobuf will automatically be a dependency (confirmed with `pip show pgoapi`) - Even for Ubuntu 16.04 LTS, python-protobuf version is 2.6.1-1.3, which is too old + confirmed with `apt-cache show python-protobuf` + see issue #1815 (https://github.com/PokemonGoF/PokemonGo-Bot/issues/1815) --- Dockerfile | 2 -- install.sh | 2 +- setup.sh | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2520813273..141325d60e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,6 @@ RUN echo $timezone > /etc/timezone \ && ln -sfn /usr/share/zoneinfo/$timezone /etc/localtime \ && dpkg-reconfigure -f noninteractive tzdata -RUN apt-get update \ - && apt-get install -y python-protobuf RUN cd /tmp && wget "http://pgoapi.com/pgoencrypt.tar.gz" \ && tar zxvf pgoencrypt.tar.gz \ && cd pgoencrypt/src \ diff --git a/install.sh b/install.sh index 1e7415ed30..f11e58e136 100755 --- a/install.sh +++ b/install.sh @@ -5,7 +5,7 @@ if [ -f /etc/debian_version ] then echo "You are on Debian/Ubuntu" sudo apt-get update -sudo apt-get -y install python python-pip python-dev build-essential git python-protobuf virtualenv +sudo apt-get -y install python python-pip python-dev build-essential git virtualenv elif [ -f /etc/redhat-release ] then echo "You are on CentOS/RedHat" diff --git a/setup.sh b/setup.sh index c5acdf9c8b..c9ec2a1c5e 100755 --- a/setup.sh +++ b/setup.sh @@ -55,7 +55,7 @@ if [ -f /etc/debian_version ] then echo "You are on Debian/Ubuntu" sudo apt-get update -sudo apt-get -y install python python-pip python-dev build-essential git python-protobuf virtualenv +sudo apt-get -y install python python-pip python-dev build-essential git virtualenv elif [ -f /etc/redhat-release ] then echo "You are on CentOS/RedHat" From b8ea3681013b152f472fcec89b9d83436e33cff8 Mon Sep 17 00:00:00 2001 From: Anakin5 Date: Fri, 12 Aug 2016 15:19:04 +0800 Subject: [PATCH 176/202] First basic features of the pokemon optimizer (#2956) * catching every single pokemon nearby * catch lured pokemon in all forts nearby * adding run_interval to some tasks to avoid running all the time and minimum tick time of 5 seconds Tasks inheriting from BaseTask should use `self._update_last_ran` and `_time_to_run` if they want to implement the time based running. The config to set a custom timer is named `run_interval`. * added config to ignore item count for Spin and MoveToFort this works good with the `run_interval` configuration added to TransferPokemon and RecycleItem * spinning all pokestops in range * fixing loop in spin fort task * First basic features of the pokemon optimizer * For now, dry run only * Add cygwin to supported platform and improved log readability (#2948) * Add cygwin to supported platform and improved log readability * fixed formatting * - Add dry_run and use_lucky_egg in config - Evolve all pokemons together and only if enough for a full lucky egg (90). - Keep enough candies for consecutive evolutions of best pokemons - Only evolve the lowest rank of a family * Add lucky egg support when enough pokemon to evolve * fixing returns * - Support Eevee evolution scheme - Rename "use_lucky_egg" parameter in the more accurate "evolve_only_with_lucky_egg" * Revert "Merge remote-tracking branch 'origin/faeture/xp-improvements' into pokemon_optimizer" This reverts commit ff1f5e4bd3ec66b904625ec26b969f57ae6aaeb8, reversing changes made to e8fd90137e53409e87f8fdcf341916cf6d551481. * - Fix an issue in evolve_pokemon task - Use common inventory - Add configuration example * Add missing inventory refresh at the end of the process * Add missing inventory refresh after catching a pokemon * Add parameters "transfer" and "evolve" to activate/deactivate corresponding action. If both false, this is equivalent to a dry_run. Add parameters "use_lucky_egg" to allow task to use a lucky egg before evolve. Add parameter "minimum_evolve_for_lucky_egg" to add a requirement on the number of evolution before using a lucky egg. * Move some functions around * Default lucky egg to false + had again parameter "evolve_only_with_lucky_egg" * Fix qn issue with egg counting Add configuration parameter to allow customization of how pokemons are ranked in a family * Update configuration example * Upgrade to latest inventory * Fix bug --- .gitignore | 1 + configs/config.json.optimizer.example | 105 +++++++ pokemongo_bot/__init__.py | 2 +- pokemongo_bot/cell_workers/__init__.py | 1 + pokemongo_bot/cell_workers/evolve_pokemon.py | 3 +- .../cell_workers/pokemon_catch_worker.py | 3 +- .../cell_workers/pokemon_optimizer.py | 285 ++++++++++++++++++ 7 files changed, 396 insertions(+), 4 deletions(-) create mode 100644 configs/config.json.optimizer.example create mode 100644 pokemongo_bot/cell_workers/pokemon_optimizer.py diff --git a/.gitignore b/.gitignore index 06973c1249..cfba4942e0 100644 --- a/.gitignore +++ b/.gitignore @@ -119,6 +119,7 @@ configs/* !configs/config.json.map.example !configs/path.example.json !config.json.cluster.example +!config.json.optimizer.example # Virtualenv folders bin/ diff --git a/configs/config.json.optimizer.example b/configs/config.json.optimizer.example new file mode 100644 index 0000000000..f63a2b7ae1 --- /dev/null +++ b/configs/config.json.optimizer.example @@ -0,0 +1,105 @@ +{ + "auth_service": "google", + "username": "YOUR_USERNAME", + "password": "YOUR_PASSWORD", + "location": "SOME_LOCATION", + "gmapkey": "GOOGLE_MAPS_API_KEY", + "tasks": [ + { + "type": "HandleSoftBan" + }, + { + "type": "CollectLevelUpReward" + }, + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + }, + { + "type": "PokemonOptimizer", + "config": { + "transfer": true, + "evolve": true, + "use_lucky_egg": true, + "evolve_only_with_lucky_egg": true, + "minimum_evolve_for_lucky_egg": 90, + "keep": [ + { + "top": 1, + "evolve": true, + "// Available sorting keys are:": true, + "// iv, cp, ncp, ivcp, max_cp, iv_attack, iv_defense, iv_stamina, hp_max, level": true, + "sort": ["iv"] + }, + { + "top": 1, + "evolve": true, + "sort": ["ncp"] + }, + { + "top": 1, + "evolve": false, + "sort": ["cp"] + } + ] + } + }, + { + "type": "RecycleItems", + "config": { + "min_empty_space": 15, + "item_filter": { + "Pokeball": { "keep": 100 }, + "Potion": { "keep": 10 }, + "Super Potion": { "keep": 20 }, + "Hyper Potion": { "keep": 30 }, + "Revive": { "keep": 30 }, + "Razz Berry": { "keep": 100 } + } + } + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort", + "config": { + "ignore_item_count": true + } + }, + { + "type": "MoveToFort", + "config": { + "lure_attraction": false, + "lure_max_distance": 2000, + "ignore_item_count": true + } + } + ], + "map_object_cache_time": 5, + "forts": { + "avoid_circles": true, + "max_circle_size": 50 + }, + "websocket_server": true, + "walk": 4.16, + "action_wait_min": 1, + "action_wait_max": 4, + "debug": false, + "test": false, + "health_record": false, + "location_cache": true, + "distance_unit": "km", + "reconnecting_timeout": 15, + "logging_color": true, + "catch": { + "any": { + "always_catch": true + } + } +} diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 5d10cfba02..f49d698464 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -299,7 +299,7 @@ def _register_events(self): ) self.event_manager.register_event( 'pokemon_evolved', - parameters=('pokemon', 'iv', 'cp') + parameters=('pokemon', 'iv', 'cp', 'xp') ) self.event_manager.register_event('skip_evolve') self.event_manager.register_event('threw_berry_failed', parameters=('status_code',)) diff --git a/pokemongo_bot/cell_workers/__init__.py b/pokemongo_bot/cell_workers/__init__.py index 7933425b63..538673cfe7 100644 --- a/pokemongo_bot/cell_workers/__init__.py +++ b/pokemongo_bot/cell_workers/__init__.py @@ -8,6 +8,7 @@ from move_to_map_pokemon import MoveToMapPokemon from nickname_pokemon import NicknamePokemon from pokemon_catch_worker import PokemonCatchWorker +from pokemon_optimizer import PokemonOptimizer from transfer_pokemon import TransferPokemon from recycle_items import RecycleItems from spin_fort import SpinFort diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 7380f1c5db..3f764f9d58 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -106,7 +106,8 @@ def _execute_pokemon_evolve(self, pokemon, cache): data={ 'pokemon': pokemon.name, 'iv': pokemon.iv, - 'cp': pokemon.cp + 'cp': pokemon.cp, + 'xp': 0 } ) inventory.candies().get(pokemon.pokemon_id).consume(pokemon.evolution_cost) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 451ca0e9df..d28f3f58fd 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -387,8 +387,7 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): ) # We could refresh here too, but adding 3 saves a inventory request - candy = inventory.candies().get(pokemon.num) - candy.add(3) + candy = inventory.candies(True).get(pokemon.num) self.emit_event( 'gained_candy', formatted='You now have {quantity} {type} candy!', diff --git a/pokemongo_bot/cell_workers/pokemon_optimizer.py b/pokemongo_bot/cell_workers/pokemon_optimizer.py new file mode 100644 index 0000000000..ba7072eb2d --- /dev/null +++ b/pokemongo_bot/cell_workers/pokemon_optimizer.py @@ -0,0 +1,285 @@ +import copy +import logging + +from pokemongo_bot import inventory +from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.human_behaviour import sleep, action_delay +from pokemongo_bot.item_list import Item +from pokemongo_bot.worker_result import WorkerResult + + +class PokemonOptimizer(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + + def initialize(self): + self.family_by_family_id = {} + self.logger = logging.getLogger(self.__class__.__name__) + + self.config_transfer = self.config.get("transfer", False) + self.config_evolve = self.config.get("evolve", False) + self.config_use_lucky_egg = self.config.get("use_lucky_egg", False) + self.config_evolve_only_with_lucky_egg = self.config.get("evolve_only_with_lucky_egg", True) + self.config_minimum_evolve_for_lucky_egg = self.config.get("minimum_evolve_for_lucky_egg", 90) + self.config_keep = self.config.get("keep", [{"top": 1, "evolve": True, "sort": ["iv"]}, + {"top": 1, "evolve": True, "sort": ["ncp"]}, + {"top": 1, "evolve": False, "sort": ["cp"]}]) + + def get_pokemon_slot_left(self): + return self.bot._player["max_pokemon_storage"] - len(inventory.pokemons()._data) + + def work(self): + if self.get_pokemon_slot_left() > 5: + return WorkerResult.SUCCESS + + self.parse_inventory() + + transfer_all = [] + evo_all_best = [] + evo_all_crap = [] + + for family_id, family in self.family_by_family_id.items(): + transfer, evo_best, evo_crap = self.get_family_optimized(family_id, family) + transfer_all += transfer + evo_all_best += evo_best + evo_all_crap += evo_crap + + evo_all = evo_all_best + evo_all_crap + + self.apply_optimization(transfer_all, evo_all) + inventory.refresh_inventory() + + return WorkerResult.SUCCESS + + def parse_inventory(self): + self.family_by_family_id.clear() + + for pokemon in inventory.pokemons().all(): + family_id = pokemon.first_evolution_id + setattr(pokemon, "ncp", pokemon.cp_percent) + + self.family_by_family_id.setdefault(family_id, []).append(pokemon) + + def get_family_optimized(self, family_id, family): + if family_id == 133: # "Eevee" + return self.get_multi_family_optimized(family_id, family, 3) + + evolve_best = [] + keep_best = [] + + for criteria in self.config_keep: + if criteria.get("evolve", True): + evolve_best += self.get_top_rank(family, criteria) + else: + keep_best += self.get_top_rank(family, criteria) + + evolve_best = self.unique_pokemons(evolve_best) + keep_best = self.unique_pokemons(keep_best) + + return self.get_evolution_plan(family_id, family, evolve_best, keep_best) + + def get_multi_family_optimized(self, family_id, family, nb_branch): + # Transfer each group of senior independently + senior_family = [p for p in family if not p.has_next_evolution()] + other_family = [p for p in family if p.has_next_evolution()] + senior_pids = set(p.pokemon_id for p in senior_family) + senior_grouped_family = {pid: [p for p in senior_family if p.pokemon_id == pid] for pid in senior_pids} + + transfer_senior = [] + + for senior_pid, senior_family in senior_grouped_family.items(): + transfer_senior += self.get_family_optimized(senior_pid, senior_family)[0] + + if len(senior_pids) < nb_branch: + # We did not get every combination yet = All other Pokemons are potentially good to keep + evolve_best = other_family + evolve_best.sort(key=lambda p: p.iv * p.ncp, reverse=True) + keep_best = [] + else: + evolve_best = [] + keep_best = [] + + for criteria in self.config_keep: + top = [] + + for f in senior_grouped_family.values(): + top += self.get_top_rank(f, criteria) + + worst = self.get_sorted_family(top, criteria)[-1] + + if criteria.get("evolve", True): + evolve_best += self.get_better_rank(family, criteria, worst) + else: + keep_best += self.get_better_rank(family, criteria, worst) + + evolve_best = self.unique_pokemons(evolve_best) + keep_best = self.unique_pokemons(keep_best) + + transfer, evo_best, evo_crap = self.get_evolution_plan(family_id, other_family, evolve_best, keep_best) + transfer += transfer_senior + + return (transfer, evo_best, evo_crap) + + def get_top_rank(self, family, criteria): + sorted_family = self.get_sorted_family(family, criteria) + worst = sorted_family[criteria.get("top", 1) - 1] + return [p for p in sorted_family if self.get_rank(p, criteria) >= self.get_rank(worst, criteria)] + + def get_better_rank(self, family, criteria, worst): + return [p for p in self.get_sorted_family(family, criteria) if self.get_rank(p, criteria) >= self.get_rank(worst, criteria)] + + def get_sorted_family(self, family, criteria): + return sorted(family, key=lambda p: self.get_rank(p, criteria), reverse=True) + + def get_rank(self, pokemon, criteria): + return tuple(getattr(pokemon, a, None) for a in criteria.get("sort")) + + def get_pokemon_max_cp(self, pokemon_name): + return int(self.pokemon_max_cp.get(pokemon_name, 0)) + + def unique_pokemons(self, l): + seen = set() + return [p for p in l if not (p.id in seen or seen.add(p.id))] + + def get_evolution_plan(self, family_id, family, evolve_best, keep_best): + candies = inventory.candies().get(family_id).quantity + + # All the rest is crap, for now + crap = family[:] + crap = [p for p in crap if p not in evolve_best] + crap = [p for p in crap if p not in keep_best] + crap.sort(key=lambda p: p.iv, reverse=True) + + candies += len(crap) + + # Let's see if we can evolve our best pokemons + can_evolve_best = [] + + for pokemon in evolve_best: + if not pokemon.has_next_evolution(): + continue + + candies -= pokemon.evolution_cost + + if candies < 0: + continue + + can_evolve_best.append(pokemon) + + # Not sure if the evo keep the same id + next_pid = pokemon.next_evolution_ids[0] + next_evo = copy.copy(pokemon) + next_evo.pokemon_id = next_pid + next_evo._static_data = inventory.pokemons().data_for(next_pid) + next_evo.name = inventory.pokemons().name_for(next_pid) + evolve_best.append(next_evo) + + # Compute how many crap we should keep if we want to batch evolve them for xp + junior_evolution_cost = inventory.pokemons().evolution_cost_for(family_id) + + # transfer + keep_for_evo = len(crap) + # leftover_candies = candies - len(crap) + transfer * 1 + # keep_for_evo = leftover_candies / junior_evolution_cost + # + # keep_for_evo = (candies - len(crap) + transfer) / junior_evolution_cost + # keep_for_evo = (candies - keep_for_evo) / junior_evolution_cost + + if (candies > 0) and junior_evolution_cost: + keep_for_evo = int(candies / (junior_evolution_cost + 1)) + else: + keep_for_evo = 0 + + evo_crap = [p for p in crap if p.has_next_evolution() and p.evolution_cost == junior_evolution_cost][:keep_for_evo] + transfer = [p for p in crap if p not in evo_crap] + + return (transfer, can_evolve_best, evo_crap) + + def apply_optimization(self, transfer, evo): + for pokemon in transfer: + self.transfer_pokemon(pokemon) + + if self.config_evolve and self.config_use_lucky_egg: + lucky_egg = inventory.items().get(Item.ITEM_LUCKY_EGG.value) # @UndefinedVariable + + if lucky_egg.count == 0: + if self.config_evolve_only_with_lucky_egg: + self.logger.info("Skipping evolution step. No lucky egg available") + return + elif len(evo) >= self.config_minimum_evolve_for_lucky_egg: + self.use_lucky_egg() + + self.logger.info("Evolving %s Pokemons", len(evo)) + + for pokemon in evo: + self.evolve_pokemon(pokemon) + + def transfer_pokemon(self, pokemon): + if self.config_transfer: + self.bot.api.release_pokemon(pokemon_id=pokemon.id) + else: + pass + + self.emit_event("pokemon_release", + formatted="Exchanged {pokemon} [IV {iv}] [CP {cp}]", + data={"pokemon": pokemon.name, + "iv": pokemon.iv, + "cp": pokemon.cp}) + + if self.config_transfer: + inventory.candies().get(pokemon.pokemon_id).add(1) + action_delay(self.bot.config.action_wait_min, self.bot.config.action_wait_max) + + def use_lucky_egg(self): + lucky_egg = inventory.items().get(Item.ITEM_LUCKY_EGG.value) # @UndefinedVariable + + if self.config_evolve and self.config_use_lucky_egg: + response_dict = self.bot.use_lucky_egg() + lucky_egg.remove(1) + else: + response_dict = {"responses": {"USE_ITEM_XP_BOOST": {"result": 1}}} + + if not response_dict: + self.emit_event("lucky_egg_error", + level='error', + formatted="Failed to use lucky egg!") + return False + + result = response_dict.get("responses", {}).get("USE_ITEM_XP_BOOST", {}).get("result", 0) + + if result == 1: + self.emit_event("used_lucky_egg", + formatted="Used lucky egg ({amount_left} left).", + data={"amount_left": lucky_egg.count}) + return True + else: + self.emit_event("lucky_egg_error", + level='error', + formatted="Failed to use lucky egg!") + return False + + def evolve_pokemon(self, pokemon): + if self.config_evolve: + response_dict = self.bot.api.evolve_pokemon(pokemon_id=pokemon.id) + else: + response_dict = {"responses": {"EVOLVE_POKEMON": {"result": 1}}} + + if not response_dict: + return False + + result = response_dict.get("responses", {}).get("EVOLVE_POKEMON", {}).get("result", 0) + xp = response_dict.get("responses", {}).get("EVOLVE_POKEMON", {}).get("experience_awarded", 0) + + if result == 1: + self.emit_event("pokemon_evolved", + formatted="Evolved {pokemon} [IV {iv}] [CP {cp}] [+{xp} xp]", + data={"pokemon": pokemon.name, + "iv": pokemon.iv, + "cp": pokemon.cp, + "xp": xp}) + + if self.config_evolve: + inventory.candies().get(pokemon.pokemon_id).consume(pokemon.evolution_cost) + sleep(20) + + return True + else: + return False From ac07ad368c3d11d8b2cca2cdd9e18b7395054514 Mon Sep 17 00:00:00 2001 From: Vianney Dubus Date: Fri, 12 Aug 2016 09:44:54 +0200 Subject: [PATCH 177/202] NicknamePokemon: Format iv_pct on 3 digits (#3698) For better sorting on pokemon's name, format iv_pct on 3 digits. --- pokemongo_bot/cell_workers/nickname_pokemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py index e521cadfba..68943e7a8e 100644 --- a/pokemongo_bot/cell_workers/nickname_pokemon.py +++ b/pokemongo_bot/cell_workers/nickname_pokemon.py @@ -50,7 +50,7 @@ def _nickname_pokemon(self,pokemon): iv_list = [iv_attack,iv_defense,iv_stamina] iv_ads = "/".join(map(str,iv_list)) iv_sum = sum(iv_list) - iv_pct = "{:0.0f}".format(100*iv_sum/45.0) + iv_pct = "{:03.0f}".format(100*iv_sum/45.0) log_color = 'red' try: new_name = self.template.format(name=name, From 50cd7bf243eab90b88a2d8bd99e105b39647f331 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 12 Aug 2016 04:59:57 -0400 Subject: [PATCH 178/202] Config/encrypt fix (#3707) * Fix typo in config * Fix all configs * Fixed __init__.py thanks to @hklcf --- configs/config.json.cluster.example | 2 +- configs/config.json.example | 2 +- configs/config.json.map.example | 2 +- configs/config.json.optimizer.example | 1 + configs/config.json.path.example | 2 +- configs/config.json.pokemon.example | 2 +- pokemongo_bot/__init__.py | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index bb01bb44f3..c9cc3b3ca3 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -4,7 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", - "libencrypt_location": "", + "encrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.example b/configs/config.json.example index 502c18c78e..2cf5a0f737 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -4,7 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", - "libencrypt_location": "", + "encrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.map.example b/configs/config.json.map.example index acb6b2f5ea..ff2b8a69e2 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -4,7 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", - "libencrypt_location": "", + "encrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.optimizer.example b/configs/config.json.optimizer.example index f63a2b7ae1..4db7f2cee6 100644 --- a/configs/config.json.optimizer.example +++ b/configs/config.json.optimizer.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "encrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 52286b2809..23578f8125 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -4,7 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", - "libencrypt_location": "", + "encrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index c271f21fcc..a2d5d96a2b 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -4,7 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", - "libencrypt_location": "", + "encrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index f49d698464..7d8a207ece 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -670,7 +670,7 @@ def get_encryption_lib(self): full_path = path + '/'+ file_name if not os.path.isfile(full_path): - self.logger.error(file_name + ' is not found! Please place it in the bots root directory or set libencrypt_location in config.') + self.logger.error(file_name + ' is not found! Please place it in the bots root directory or set encrypt_location in config.') self.logger.info('Platform: '+ _platform + ' Encrypt.so directory: '+ path) sys.exit(1) else: From 62715ae7f9385b4557e304cfc420b25ec34dde62 Mon Sep 17 00:00:00 2001 From: Chris Wild Date: Fri, 12 Aug 2016 14:09:30 +0100 Subject: [PATCH 179/202] Fixed EventManager handlers to be list instead of tuple (#3734) --- pokemongo_bot/event_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/event_manager.py b/pokemongo_bot/event_manager.py index 3773ec8a9e..3d759bf666 100644 --- a/pokemongo_bot/event_manager.py +++ b/pokemongo_bot/event_manager.py @@ -23,7 +23,7 @@ class EventManager(object): def __init__(self, *handlers): self._registered_events = dict() - self._handlers = handlers or [] + self._handlers = list(handlers) or [] def event_report(self): for event, parameters in self._registered_events.iteritems(): From fff1eacec9d59c90bd73c881e86ac07c91345c23 Mon Sep 17 00:00:00 2001 From: nivong Date: Fri, 12 Aug 2016 16:40:53 +0200 Subject: [PATCH 180/202] Heaps of updates to docs and other small errors in running the bot. (#3593) * Update setup.sh * fixed for mac creating encrypt.so * for now just do wget or curl * this is all in the setup.sh * updated instructions to reflect setup.sh changes * Update installation.md * Update CONTRIBUTORS.md * Update setup.sh * Update installation.md * Update installation.md * Update installation.md * added missing submodule update * Update installation.md * Update installation.md * Update installation.md * Delete install.sh * Update .gitignore * Update installation.md * Update setup.sh * Update installation.md * Update run.sh add `source bin/activate` if someone forget to use virtualenv. --- CONTRIBUTORS.md | 2 ++ docs/installation.md | 31 ++++++++++----------- install.sh | 64 -------------------------------------------- run.sh | 7 +++-- setup.sh | 9 +++++-- 5 files changed, 26 insertions(+), 87 deletions(-) delete mode 100755 install.sh diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 725360784b..14a7d95514 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -64,5 +64,7 @@ * cmezh * Nivong * kestel + * simonsmh * joaodragao * extink + diff --git a/docs/installation.md b/docs/installation.md index 873638ff54..b8951f62d2 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -9,21 +9,20 @@ #Linux/Mac Automatic installation ### Easy installation -1. Run setup.sh -e - This will create the needed encrypted.so file -2. Run setup.sh -i - This will install the bot and all stuff that is needed to run it (follow the guide) -3. Run setup.sh -c - This will make the config file needed, only basic stuff is changed here. If you want to edit more edit this file after: config/config.json -4. Run run.sh - This will run the bot and will start leveling your pokemon go account. +1. Clone the git: `git clone https://github.com/PokemonGoF/PokemonGo-Bot` +2. Go into the new directory: `cd PokemonGo-Bot` +3. Run `./setup.sh -i` + This will install the bot and all stuff that is needed to run it (follow the steps in this process) +4. Run `./run.sh` + After you are done following it this will start your bot. ### To update 1. Stop the bot if it's running. (use control + c twice to stop it) -2. Run setup.sh -r +2. Run `./setup.sh -r` This will reset and makes sure you have no changes made to any code since it will overide it -3. Run setup.sh -u - This will run git pull and will update to the new git update. +3. Rerun the bot `./run.sh` + +note: we do not support windows at this time # Manual installation @@ -31,15 +30,13 @@ - OS X: `brew update && brew install --devel protobuf` - Windows: Download protobuf 3.0: [here](https://github.com/google/protobuf/releases/download/v3.0.0-beta-4/protoc-3.0.0-beta-4-win32.zip) and unzip `bin/protoc.exe` into a folder in your PATH. -- Linux: `apt-get install python-protobuf` ### Get encrypt.so (Windows part writing need fine tune) -We don't have the copyright of encrypt.so, please grab from internet and build your self.Take the risk as your own. -Example build sequence: -Create a new separate folder some here +Due to copywrite on the encrypt.so we are not directly hosting it. Please find a copy elsewhere on the internet and compile it yourself. We accept no responsibility should you encounter any problems with files you download elsewhere. + +Ensure you are in the PokemonGo-Bot main folder and run: -wget http://pgoapi.com/pgoencrypt.tar.gz && tar -xf pgoencrypt.tar.gz && cd pgoencrypt/src/ && make -Then copy libencrypt.so to the gofbot folder and rename to encrypt.so +`wget http://pgoapi.com/pgoencrypt.tar.gz && tar -xf pgoencrypt.tar.gz && cd pgoencrypt/src/ && make && mv libencrypt.so ../../encrypt.so && cd ../..` ### Note on branch Please keep in mind that master is not always up-to-date whereas 'dev' is. In the installation note below change `master` to `dev` if you want to get and use the latest version. diff --git a/install.sh b/install.sh deleted file mode 100755 index f11e58e136..0000000000 --- a/install.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -pokebotpath=$(cd "$(dirname "$0")"; pwd) -cd $pokebotpath -if [ -f /etc/debian_version ] -then -echo "You are on Debian/Ubuntu" -sudo apt-get update -sudo apt-get -y install python python-pip python-dev build-essential git virtualenv -elif [ -f /etc/redhat-release ] -then -echo "You are on CentOS/RedHat" -sudo yum -y install epel-release -sudo yum -y install python-pip -elif [ "$(uname -s)" == "Darwin" ] -then -echo "You are on Mac os" -sudo brew update -sudo brew install --devel protobuf -else -echo "Please check if you have python pip protobuf gcc make installed on your device." -echo "Wait 5 seconds to continue or Use ctrl+c to interrupt this shell." -sleep 5 -fi -pip install virtualenv -cd $pokebotpath -git pull -git submodule init -git submodule foreach git pull origin master -virtualenv . -source bin/activate -pip install -r requirements.txt -echo "Start to make encrypt.so" -wget http://pgoapi.com/pgoencrypt.tar.gz -tar -xf pgoencrypt.tar.gz -cd pgoencrypt/src/ -make -mv libencrypt.so $pokebotpath/encrypt.so -cd ../.. -rm -rf pgoencrypt.tar.gz -rm -rf pgoencrypt -echo "Install complete. Starting to generate config.json." -cd $pokebotpath -read -p "1.google 2.ptc -" auth -read -p "Input username -" username -read -p "Input password -" -s password -read -p " -Input location -" location -read -p "Input gmapkey -" gmapkey -cp configs/config.json.example configs/config.json -if [ "$auth" = "2" ] -then -sed -i "s/google/ptc/g" configs/config.json -fi -sed -i "s/YOUR_USERNAME/$username/g" configs/config.json -sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json -sed -i "s/SOME_LOCATION/$location/g" configs/config.json -sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json -echo "Edit configs/config.json to modify any other config. Use run.sh ./configs/config.json to run." -exit 0 diff --git a/run.sh b/run.sh index 9938f8c5e0..fa6d8ffab1 100755 --- a/run.sh +++ b/run.sh @@ -6,14 +6,13 @@ filename=$1 else filename="./configs/config.json" fi - +cd $pokebotpath +source bin/activate if [ ! -f "$filename" ]; then -echo "There's no "$filename" file. use setup.sh -config to creat one." +echo "There's no "$filename" file. Please use ./setup.sh -c to creat one." fi - while true do -cd $pokebotpath python pokecli.py -cf $filename echo `date`" Pokebot "$*" Stopped." read -p "Press any button or wait 20 seconds to continue. diff --git a/setup.sh b/setup.sh index c9ec2a1c5e..9535bc7d10 100755 --- a/setup.sh +++ b/setup.sh @@ -1,11 +1,12 @@ #!/usr/bin/env bash +#encoding=utf8 pokebotpath=$(cd "$(dirname "$0")"; pwd) backuppath=$pokebotpath"/backup" function Pokebotupdate () { cd $pokebotpath git pull -git submodule init +git submodule update --init --recursive git submodule foreach git pull origin master virtualenv . source bin/activate @@ -14,7 +15,11 @@ pip install -r requirements.txt function Pokebotencrypt () { echo "Start to make encrypt.so" -wget http://pgoapi.com/pgoencrypt.tar.gz +if [ "$(uname -s)" == "Darwin" ]; then #Mac platform + curl -O http://pgoapi.com/pgoencrypt.tar.gz +elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then #GNU/Linux platform + wget http://pgoapi.com/pgoencrypt.tar.gz +fi tar -xf pgoencrypt.tar.gz cd pgoencrypt/src/ make From ee9c6f3a1f22797c83dd34ce6f70cf4b810d1fcc Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Fri, 12 Aug 2016 17:19:59 +0200 Subject: [PATCH 181/202] Update docker.md --- docs/docker.md | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/docs/docker.md b/docs/docker.md index 2b2181bb56..11f8634744 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,42 +1,48 @@ -Start by downloading for your platform: [Mac](https://www.docker.com/products/docker#/mac), [Windows](https://www.docker.com/products/docker#/windows), or [Linux](https://www.docker.com/products/docker#/linux). Once you have Docker installed, simply create the various config.json files for your different accounts (e.g. `configs/config-account1.json`) and then create a Docker image for PokemonGo-Bot using the Dockerfile in this [repo](https://hub.docker.com/r/svlentink/pokemongo-bot/). +Start by downloading for your platform: [Mac](https://www.docker.com/products/docker#/mac), [Windows](https://www.docker.com/products/docker#/windows), or [Linux](https://www.docker.com/products/docker#/linux). -#Setup -##Automatic setup -Use this docker hub url: https://hub.docker.com/r/svlentink/pokemongo-bot/ -``` -docker pull svlentink/pokemongo-bot -``` +Once you have Docker installed, simply create the various config files for your different accounts (e.g. `configs/config.json`, `configs/userdata.js`) and then create a Docker image for PokemonGo-Bot using the Dockerfile in this repo. -##Manual setup ``` cd PokemonGo-Bot -docker build -t pokemongo-bot . +docker build --build-arg timezone=Europe/London -t pokemongo-bot . ``` -#Run +Optionally you can set your timezone with the --build-arg option (default is Etc/UTC) + +After build process you can verify that the image was created with: -You can verify that the image was created with: ``` docker images ``` -- In case of automatic setup, you'll see an image called: `svlentink/pokemongo-bot` -- In case of manual setup, you'll see an image called: `pokemongo-bot` -To run PokemonGo-Bot Docker image you've created, simple run: +To run PokemonGo-Bot Docker image you've created: + ``` -docker run --name=pokego-bot1 --rm -it -v $(pwd)/configs/config-account1.json:/usr/src/app/configs/config.json -v $(pwd)/data:/usr/src/app/data pokemongo-bot +docker run --name=bot1-pokego --rm -it -v $(pwd)/configs/config.json:/usr/src/app/configs/config.json pokemongo-bot ``` -Replace `pokemongo-bot` with `svlentink/pokemongo-bot` in case you followed automatic setup. +Run a second container provided with the OpenPoGoBotWeb view: -_Check the logs in real-time `docker logs -f pgobot`_ +``` +docker run --name=bot1-pokegoweb --rm -it --volumes-from bot1-pokego -p 8000:8000 -v $(pwd)/configs/userdata.js:/usr/src/app/web/userdata.js -w /usr/src/app/web python:2.7 python -m SimpleHTTPServer +``` +The OpenPoGoWeb will be served on `http://:8000` -If you want to run multiple accounts with the same Docker image, simply specify different config.json and names in the Docker run command. -Do not push your image to a registry with your config.json and account details in it! +if docker-compose [installed](https://docs.docker.com/compose/install/) you can alternatively run the PokemonGo-Bot ecosystem with one simple command: +(by using the docker-compose.yml configuration in this repo) -Share web folder with host: ``` -docker run -it -v $(pwd)/web/:/usr/src/app/web --rm --name=pgo-bot-acct1 pokemongo-bot --config config.json +docker-compose up ``` -TODO: Add configuration for running multiple Docker containers from the same image for every bot instance, and a single container for the web UI. +Also run one single service from the compose configuration is possible: + +``` +docker-compose run --rm bot1-pokego +``` + +command for remove all stopped containers: `docker-compose rm` + +TODO: Add infos / configuration for running multiple bot instances. + +Do not push your image to a registry with your config.json and account details in it! From 6f626fa1c29d33b3c8769063cfe8a5996735258a Mon Sep 17 00:00:00 2001 From: Dmitry Ovodov Date: Sat, 13 Aug 2016 00:15:54 +0700 Subject: [PATCH 182/202] Modify pokemon_catch_worker.py to use Inventory class and fix #3411 (#3496) * Fix #3411. Update inventory info before every catch try otherwise old values used * Revert "Fix #3411. Update inventory info before every catch try otherwise old values used" This reverts commit f7678da0f68573a7397c4c55a9804ee22dcbd53e. * Modify pokemon_catch_worker.py to use Inventory class * Fix forgotten line * Fix one more forgotten line * Added check if we really used berry or not * Fix KeyError in inventory.py When we have no items of type, there are no "count" key in the dict. * Revert "Fix KeyError in inventory.py" This reverts commit ed2769c51820381044332f9e95e759bda6dc587e. * Revert "Added check if we really used berry or not" This reverts commit 42e9d9cc2c0335da0bed2adabc797f154ecc1596. * Revert "Fix one more forgotten line" This reverts commit 5fda3c49ea6483ad16bf2582dee0aed14ed34b6b. * Revert "Fix forgotten line" This reverts commit a8edc5723a1b88334beead9cd863291b894a0a49. * Revert "Modify pokemon_catch_worker.py to use Inventory class" This reverts commit 5b6e4d39bccbae6a825274080727bd7ef62b6d98. * Modify pokemon_catch_worker.py to use Inventory class and fix #3411 --- .../cell_workers/pokemon_catch_worker.py | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index d28f3f58fd..27d762caef 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -54,8 +54,7 @@ def __init__(self, pokemon, bot): self.position = bot.position self.config = bot.config self.pokemon_list = bot.pokemon_list - self.item_list = bot.item_list - self.inventory = bot.inventory + self.inventory = inventory.items() self.spawn_point_guid = '' self.response_key = '' self.response_status_key = '' @@ -204,8 +203,8 @@ def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, cu formatted='Catch rate of {catch_rate} with {ball_name} is low. Throwing {berry_name} (have {berry_count})', data={ 'catch_rate': self._pct(catch_rate_by_ball[current_ball]), - 'ball_name': self.item_list[str(current_ball)], - 'berry_name': self.item_list[str(berry_id)], + 'ball_name': self.inventory.get(current_ball).name, + 'berry_name': self.inventory.get(berry_id).name, 'berry_count': berry_count } ) @@ -227,8 +226,8 @@ def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, cu 'threw_berry', formatted="Threw a {berry_name}! Catch rate with {ball_name} is now: {new_catch_rate}", data={ - 'berry_name': self.item_list[str(berry_id)], - 'ball_name': self.item_list[str(current_ball)], + 'berry_name': self.inventory.get(berry_id).name, + 'ball_name': self.inventory.get(current_ball).name, 'new_catch_rate': self._pct(new_catch_rate_by_ball[current_ball]) } ) @@ -261,16 +260,18 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): maximum_ball = ITEM_ULTRABALL if is_vip else ITEM_GREATBALL ideal_catch_rate_before_throw = 0.9 if is_vip else 0.35 - berry_count = self.bot.item_inventory_count(berry_id) - items_stock = self.bot.current_inventory() + berry_count = self.inventory.get(ITEM_RAZZBERRY).count + ball_count = {} + for ball_id in [ITEM_POKEBALL, ITEM_GREATBALL, ITEM_ULTRABALL]: + ball_count[ball_id] = self.inventory.get(ball_id).count while True: # find lowest available ball current_ball = ITEM_POKEBALL - while items_stock[current_ball] == 0 and current_ball < maximum_ball: + while ball_count[current_ball] == 0 and current_ball < maximum_ball: current_ball += 1 - if items_stock[current_ball] == 0: + if ball_count[current_ball] == 0: self.emit_event('no_pokeballs', formatted='No usable pokeballs found!') break @@ -279,7 +280,7 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): next_ball = current_ball while next_ball < maximum_ball: next_ball += 1 - num_next_balls += items_stock[next_ball] + num_next_balls += ball_count[next_ball] # check if we've got berries to spare berries_to_spare = berry_count > 0 if is_vip else berry_count > num_next_balls + 30 @@ -287,23 +288,29 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): # use a berry if we are under our ideal rate and have berries to spare used_berry = False if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and berries_to_spare: - catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) - berry_count -= 1 - used_berry = True + new_catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) + if new_catch_rate_by_ball != catch_rate_by_ball: + catch_rate_by_ball = new_catch_rate_by_ball + self.inventory.get(ITEM_RAZZBERRY).remove(1) + berry_count -= 1 + used_berry = True # pick the best ball to catch with best_ball = current_ball while best_ball < maximum_ball: best_ball += 1 - if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and items_stock[best_ball] > 0: + if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and ball_count[best_ball] > 0: # if current ball chance to catch is under our ideal rate, and player has better ball - then use it current_ball = best_ball # if the rate is still low and we didn't throw a berry before, throw one if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and berry_count > 0 and not used_berry: - catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) - berry_count -= 1 - + new_catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) + if new_catch_rate_by_ball != catch_rate_by_ball: + catch_rate_by_ball = new_catch_rate_by_ball + self.inventory.get(ITEM_RAZZBERRY).remove(1) + berry_count -= 1 + # Randomize the quality of the throw # Default structure throw_parameters = {'normalized_reticle_size': 1.950, @@ -315,14 +322,15 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): # try to catch pokemon! # TODO : Log which type of throw we selected - items_stock[current_ball] -= 1 + ball_count[current_ball] -= 1 + self.inventory.get(current_ball).remove(1) self.emit_event( 'threw_pokeball', formatted='Used {ball_name}, with chance {success_percentage} ({count_left} left)', data={ - 'ball_name': self.item_list[str(current_ball)], + 'ball_name': self.inventory.get(current_ball).name, 'success_percentage': self._pct(catch_rate_by_ball[current_ball]), - 'count_left': items_stock[current_ball] + 'count_left': ball_count[current_ball] } ) From 59d08651ba157be2a13de586ebd760c5c4be23ea Mon Sep 17 00:00:00 2001 From: joaodragao Date: Fri, 12 Aug 2016 18:17:46 +0100 Subject: [PATCH 183/202] Use Ultraball If No Other Balls (With Constraint) (#3421) * Add Use Ultraball (#1) * Add `use_ultraball` event to Event Manager * Add use ultraball if pokeball + greatball = 0 * Add Use Ultraball if No Other Balls (#2) * Add `use_ultraball` event to Event Manager * Add use ultraball if pokeball + greatball = 0 * Add New Contributor * Revert "Add Use Ultraball" (#4) * Use Ultraball If No Other Balls (#3) * Add `use_ultraball` event to Event Manager * Add use ultraball if pokeball + greatball = 0 * Add New Contributor * Remove 'use_ultraball' event. * Remove `use_ultraball` event call * Update & add avoid catching Pokemon if no pokeball * Update conflict contributors * Add get `min_ultraball_to_keep` from config file * Improved `min_ultraball_to_keep` with condition * Added `min_ultraball_to_keep` option * Added `min_ultraball_to_keep` option * Added `min_ultraball_to_keep` option * Added `min_ultraball_to_keep` option * Add `min_ultraball_to_keep` option * Remove count all pokeballs * Resolved Conflicts --- configs/config.json.cluster.example | 1 + configs/config.json.example | 3 +++ configs/config.json.map.example | 1 + configs/config.json.path.example | 1 + configs/config.json.pokemon.example | 1 + pokecli.py | 1 + pokemongo_bot/cell_workers/pokemon_catch_worker.py | 14 +++++++++++++- 7 files changed, 21 insertions(+), 1 deletion(-) diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index c9cc3b3ca3..1bedbcbab9 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -100,6 +100,7 @@ "reconnecting_timeout": 15, "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "min_ultraball_to_keep": 10, "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, diff --git a/configs/config.json.example b/configs/config.json.example index 2cf5a0f737..f306d3a920 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -113,6 +113,9 @@ "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, + "catch_randomize_reticle_factor": 1.0, + "catch_randomize_spin_factor": 1.0, + "min_ultraball_to_keep": 10, "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, diff --git a/configs/config.json.map.example b/configs/config.json.map.example index ff2b8a69e2..6051e063cc 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -342,6 +342,7 @@ "reconnecting_timeout": 15, "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "min_ultraball_to_keep": 10, "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 23578f8125..2581862b31 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -102,6 +102,7 @@ "reconnecting_timeout": 15, "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "min_ultraball_to_keep": 10, "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index a2d5d96a2b..2ad81a7369 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -108,6 +108,7 @@ "reconnecting_timeout": 15, "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "min_ultraball_to_keep": 10, "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or" }, diff --git a/pokecli.py b/pokecli.py index 2cd5553ba5..4ccedb5fa6 100644 --- a/pokecli.py +++ b/pokecli.py @@ -454,6 +454,7 @@ def _json_loader(filename): config.action_wait_min = load.get('action_wait_min', 1) config.plugins = load.get('plugins', []) config.raw_tasks = load.get('tasks', []) + config.min_ultraball_to_keep = load.get('min_ultraball_to_keep', None) config.vips = load.get('vips', {}) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 27d762caef..c950b5bac4 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -265,6 +265,12 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): for ball_id in [ITEM_POKEBALL, ITEM_GREATBALL, ITEM_ULTRABALL]: ball_count[ball_id] = self.inventory.get(ball_id).count + # use `min_ultraball_to_keep` from config if is not None + min_ultraball_to_keep = items_stock[ITEM_ULTRABALL] + if self.config.min_ultraball_to_keep is not None: + if self.config.min_ultraball_to_keep >= 0 and self.config.min_ultraball_to_keep < min_ultraball_to_keep: + min_ultraball_to_keep = self.config.min_ultraball_to_keep + while True: # find lowest available ball @@ -273,7 +279,13 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): current_ball += 1 if ball_count[current_ball] == 0: self.emit_event('no_pokeballs', formatted='No usable pokeballs found!') - break + + # use untraball if there is no other balls with constraint to `min_ultraball_to_keep` + if maximum_ball != ITEM_ULTRABALL and items_stock[ITEM_ULTRABALL] > min_ultraball_to_keep: + maximum_ball = ITEM_ULTRABALL + continue + else: + break # check future ball count num_next_balls = 0 From 76587deac79ead429312243845d78d7978c4d6b8 Mon Sep 17 00:00:00 2001 From: achretien Date: Fri, 12 Aug 2016 19:30:43 +0200 Subject: [PATCH 184/202] Add and Remove pokemon from the inventory cache when catch, release and evolve (#3738) * Add and Remove pokemon from the inventory cache when catch and release * Add dealing with evolved pokemon also * Add the evolved pokemon --- pokemongo_bot/cell_workers/evolve_pokemon.py | 7 +++- .../cell_workers/pokemon_catch_worker.py | 34 ++++++------------- .../cell_workers/transfer_pokemon.py | 8 ++++- pokemongo_bot/inventory.py | 18 +++++++++- 4 files changed, 41 insertions(+), 26 deletions(-) diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 3f764f9d58..b5675aba8f 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -1,5 +1,6 @@ from pokemongo_bot import inventory from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.inventory import Pokemon from pokemongo_bot.item_list import Item from pokemongo_bot.base_task import BaseTask @@ -110,7 +111,11 @@ def _execute_pokemon_evolve(self, pokemon, cache): 'xp': 0 } ) - inventory.candies().get(pokemon.pokemon_id).consume(pokemon.evolution_cost) + awarded_candies = response_dict.get('responses', {}).get('EVOLVE_POKEMON', {}).get('candy_awarded', 0) + inventory.candies().get(pokemon.pokemon_id).consume(pokemon.evolution_cost - awarded_candies) + inventory.pokemons().remove(pokemon.id) + pokemon = Pokemon(response_dict.get('responses', {}).get('EVOLVE_POKEMON', {}).get('evolved_pokemon_data', {})) + inventory.pokemons().add(pokemon) 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 c950b5bac4..1e5b161d13 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -5,6 +5,7 @@ from pokemongo_bot import inventory from pokemongo_bot.base_task import BaseTask from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.inventory import Pokemon from pokemongo_bot.worker_result import WorkerResult CATCH_STATUS_SUCCESS = 1 @@ -26,25 +27,6 @@ } -class Pokemon(object): - - def __init__(self, pokemon_list, pokemon_data): - 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) - self.stamina = pokemon_data.get('individual_stamina', 0) - - @property - def iv(self): - return round((self.attack + self.defense + self.stamina) / 45.0, 2) - - @property - def iv_display(self): - return '{}/{}/{}'.format(self.attack, self.defense, self.stamina) - - class PokemonCatchWorker(BaseTask): def __init__(self, pokemon, bot): @@ -84,7 +66,7 @@ def work(self, response_dict=None): # get pokemon data pokemon_data = response['wild_pokemon']['pokemon_data'] if 'wild_pokemon' in response else response['pokemon_data'] - pokemon = Pokemon(self.pokemon_list, pokemon_data) + pokemon = Pokemon(pokemon_data) # skip ignored pokemon if not self._should_catch_pokemon(pokemon): @@ -102,7 +84,7 @@ def work(self, response_dict=None): 'encounter_id': self.pokemon['encounter_id'], 'latitude': self.pokemon['latitude'], 'longitude': self.pokemon['longitude'], - 'pokemon_id': pokemon.num + 'pokemon_id': pokemon.pokemon_id } ) @@ -256,6 +238,10 @@ def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, cu def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): # settings that may be exposed at some point + """ + + :type pokemon: Pokemon + """ berry_id = ITEM_RAZZBERRY maximum_ball = ITEM_ULTRABALL if is_vip else ITEM_GREATBALL ideal_catch_rate_before_throw = 0.9 if is_vip else 0.35 @@ -389,7 +375,9 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): # pokemon caught! elif catch_pokemon_status == CATCH_STATUS_SUCCESS: + pokemon.id = response_dict['responses']['CATCH_POKEMON']['captured_pokemon_id'] self.bot.metrics.captured_pokemon(pokemon.name, pokemon.cp, pokemon.iv_display, pokemon.iv) + inventory.pokemons().add(pokemon) self.emit_event( 'pokemon_caught', formatted='Captured {pokemon}! [CP {cp}] [Potential {iv}] [{iv_display}] [+{exp} exp]', @@ -402,12 +390,12 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): 'encounter_id': self.pokemon['encounter_id'], 'latitude': self.pokemon['latitude'], 'longitude': self.pokemon['longitude'], - 'pokemon_id': pokemon.num + 'pokemon_id': pokemon.pokemon_id } ) # We could refresh here too, but adding 3 saves a inventory request - candy = inventory.candies(True).get(pokemon.num) + candy = inventory.candies(True).get(pokemon.pokemon_id) self.emit_event( 'gained_candy', formatted='You now have {quantity} {type} candy!', diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 9e970d7d7f..a8eb62c34a 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -4,7 +4,7 @@ from pokemongo_bot import inventory from pokemongo_bot.human_behaviour import action_delay from pokemongo_bot.base_task import BaseTask -from pokemongo_bot.inventory import Pokemons +from pokemongo_bot.inventory import Pokemons, Pokemon class TransferPokemon(BaseTask): @@ -66,6 +66,7 @@ def work(self): def _release_pokemon_get_groups(self): pokemon_groups = {} + # TODO: Use new inventory everywhere and then remove the inventory update for pokemon in inventory.pokemons(True).all(): if pokemon.in_fort or pokemon.is_favorite: continue @@ -134,6 +135,10 @@ def should_release_pokemon(self, pokemon, keep_best_mode = False): return logic_to_function[cp_iv_logic](*release_results.values()) def release_pokemon(self, pokemon): + """ + + :type pokemon: Pokemon + """ try: if self.bot.config.test: candy_awarded = 1 @@ -146,6 +151,7 @@ def release_pokemon(self, pokemon): # We could refresh here too, but adding 1 saves a inventory request candy = inventory.candies().get(pokemon.pokemon_id) candy.add(candy_awarded) + inventory.pokemons().remove(pokemon.id) self.bot.metrics.released_pokemon() self.emit_event( 'pokemon_release', diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index 3ade99c0af..c7c3dfafcd 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -341,6 +341,18 @@ def all(self): # makes caller's lives more difficult) return [p for p in super(Pokemons, self).all() if not isinstance(p, Egg)] + def add(self, pokemon): + if pokemon.id <= 0: + raise ValueError("Can't add a pokemin whitout id") + if pokemon.id in self._data: + raise ValueError("Pokemon already present in the inventory") + self._data[pokemon.id] = pokemon + + def remove(self, pokemon_id): + if pokemon_id not in self._data: + raise ValueError("Pokemon not present in the inventory") + self._data.pop(pokemon_id) + # # Static Components @@ -484,7 +496,7 @@ class Pokemon(object): def __init__(self, data): self._data = data # Unique ID for this particular Pokemon - self.id = data['id'] + self.id = data.get('id', 0) # Id of the such pokemons in pokedex self.pokemon_id = data['pokemon_id'] @@ -597,6 +609,10 @@ def candy_quantity(self): def evolution_cost(self): return Pokemons.evolution_cost_for(self.pokemon_id) + @property + def iv_display(self): + return '{}/{}/{}'.format(self.iv_attack, self.iv_defense, self.iv_stamina) + def _compute_iv_perfection(self): total_iv = self.iv_attack + self.iv_defense + self.iv_stamina iv_perfection = round((total_iv / 45.0), 2) From d580dfe39781f2590812815bf1c000b6d173cad7 Mon Sep 17 00:00:00 2001 From: PLG Date: Fri, 12 Aug 2016 19:45:10 +0200 Subject: [PATCH 185/202] Update configuration_files.md (#3742) MoveToMapPokemon behavior related to issue #3736 (discussed with @k4n30) --- docs/configuration_files.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/configuration_files.md b/docs/configuration_files.md index a881740b9a..ce9f5078a6 100644 --- a/docs/configuration_files.md +++ b/docs/configuration_files.md @@ -238,7 +238,9 @@ This task will fetch current pokemon spawns from /raw_data of an PokemonGo-Map i * `update_map` - disable/enable if the map location should be automatically updated to the bots current location * `catch` - A dictionary of pokemon to catch with an assigned priority (higher => better) * `snipe_high_prio_only` - Whether to snipe pokemon above a certain threshold. -* `snipe_high_prio_threshold` - The threshold number corresponding with the `catch` dictionary. Any pokemon above this threshold will be caught. Other will be igonored. +* `snipe_high_prio_threshold` - The threshold number corresponding with the `catch` dictionary. +* - Any pokemon above this threshold value will be caught by teleporting to its location, and getting back to original location if `snipe` is `True`. +* - Any pokemon under this threshold value will make the bot walk to the Pokemon target wether `snipe` is `True` or `False`. #### Example ``` From 12b47d367814d4d38ef3ca2cc3ae039f9ac96525 Mon Sep 17 00:00:00 2001 From: Quantra Date: Fri, 12 Aug 2016 18:45:56 +0100 Subject: [PATCH 186/202] Cache recent forts (for forts.max_circle_size) (#3556) * added bool option to cache recent forts -crf --forts.cache_recent_forts (default true) saves recent_forts in data/recent-forts-{username}.json on spin loads recent_forts from same file on start up bot doesn't start a new recent_forts on every reset * forgot contributor * typo fix no_cached_forts * changed all events related to caching forts to debug level * caching of forts happens on sigterm/exception handling of SIGTERM -Note handling of SIGTERM in python2.7 with multi threads is not reliable. Child thread can recieve SIGTERM and it is not handled in pokecli.py; pokecli.py continues to run. --- CONTRIBUTORS.md | 1 + configs/config.json.example | 3 +- pokecli.py | 44 ++++++++++++++++++++- pokemongo_bot/__init__.py | 52 +++++++++++++++++++++++++ pokemongo_bot/cell_workers/spin_fort.py | 3 ++ 5 files changed, 101 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 14a7d95514..1e463bb8d3 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -67,4 +67,5 @@ * simonsmh * joaodragao * extink + * Quantra diff --git a/configs/config.json.example b/configs/config.json.example index f306d3a920..838acc1d4b 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -101,7 +101,8 @@ "map_object_cache_time": 5, "forts": { "avoid_circles": true, - "max_circle_size": 50 + "max_circle_size": 50, + "cache_recent_forts": true }, "websocket_server": false, "walk": 4.16, diff --git a/pokecli.py b/pokecli.py index 4ccedb5fa6..ef50212d9f 100644 --- a/pokecli.py +++ b/pokecli.py @@ -33,6 +33,7 @@ import ssl import sys import time +import signal from datetime import timedelta from getpass import getpass from pgoapi.exceptions import NotLoggedInException, ServerSideRequestThrottlingException, ServerBusyOrOfflineException @@ -58,10 +59,16 @@ logger = logging.getLogger('cli') logger.setLevel(logging.INFO) +class SIGINTRecieved(Exception): pass + def main(): bot = False try: + def handle_sigint(*args): + raise SIGINTRecieved + signal.signal(signal.SIGINT, handle_sigint) + logger.info('PokemonGO Bot v1.0') sys.stdout = codecs.getwriter('utf8')(sys.stdout) sys.stderr = codecs.getwriter('utf8')(sys.stderr) @@ -95,7 +102,7 @@ def main(): while True: bot.tick() - except KeyboardInterrupt: + except (KeyboardInterrupt, SIGINTRecieved): bot.event_manager.emit( 'bot_exit', sender=bot, @@ -138,6 +145,32 @@ def main(): report_summary(bot) raise + finally: + # Cache here on SIGTERM, or Exception. Check data is available and worth caching. + if bot: + if bot.recent_forts[-1] is not None and bot.config.forts_cache_recent_forts: + cached_forts_path = os.path.join( + _base_dir, 'data', 'recent-forts-%s.json' % bot.config.username + ) + try: + with open(cached_forts_path, 'w') as outfile: + json.dump(bot.recent_forts, outfile) + bot.event_manager.emit( + 'cached_fort', + sender=bot, + level='debug', + formatted='Forts cached.', + ) + except IOError as e: + bot.event_manager.emit( + 'error_caching_forts', + sender=bot, + level='debug', + formatted='Error caching forts for {path}', + data={'path': cached_forts_path} + ) + + def report_summary(bot): if bot.metrics.start_time is None: @@ -362,6 +395,15 @@ def _json_loader(filename): type=int, default=10, ) + add_config( + parser, + load, + short_flag="-crf", + long_flag="--forts.cache_recent_forts", + help="Caches recent forts used by max_circle_size", + type=bool, + default=True, + ) add_config( parser, load, diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 7d8a207ece..dcf83bdc84 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -84,6 +84,7 @@ def start(self): self._setup_event_system() self._setup_logging() self._setup_api() + self._load_recent_forts() random.seed() @@ -456,6 +457,18 @@ def _register_events(self): parameters=('last_lat', 'last_lon') ) + # cached recent_forts + self.event_manager.register_event('loaded_cached_forts') + self.event_manager.register_event('cached_fort') + self.event_manager.register_event( + 'no_cached_forts', + parameters=('path', ) + ) + self.event_manager.register_event( + 'error_caching_forts', + parameters=('path', ) + ) + def tick(self): self.health_record.heartbeat() self.cell = self.get_meta_cell() @@ -1093,3 +1106,42 @@ def get_map_objects(self, lat, lng, timestamp, cellid): self.last_time_map_object = time.time() return self.last_map_object + + def _load_recent_forts(self): + if not self.config.forts_cache_recent_forts: + return + + + cached_forts_path = os.path.join(_base_dir, 'data', 'recent-forts-%s.json' % self.config.username) + try: + # load the cached recent forts + with open(cached_forts_path) as f: + cached_recent_forts = json.load(f) + + num_cached_recent_forts = len(cached_recent_forts) + num_recent_forts = len(self.recent_forts) + + # Handles changes in max_circle_size + if not num_recent_forts: + self.recent_forts = [] + elif num_recent_forts > num_cached_recent_forts: + self.recent_forts[-num_cached_recent_forts:] = cached_recent_forts + elif num_recent_forts < num_cached_recent_forts: + self.recent_forts = cached_recent_forts[-num_recent_forts:] + else: + self.recent_forts = cached_recent_forts + + self.event_manager.emit( + 'loaded_cached_forts', + sender=self, + level='debug', + formatted='Loaded cached forts...' + ) + except IOError: + self.event_manager.emit( + 'no_cached_forts', + sender=self, + level='debug', + formatted='Starting new cached forts for {path}', + data={'path': cached_forts_path} + ) diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 61d3eb02bd..0ba09ca633 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import json +import os import time from pgoapi.utilities import f2i @@ -9,6 +11,7 @@ from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.worker_result import WorkerResult from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.base_dir import _base_dir from utils import distance, format_time, fort_details From 4b632d84c2c4422ee0cd0342b2b882e8c1bce2bb Mon Sep 17 00:00:00 2001 From: joaodragao Date: Fri, 12 Aug 2016 18:47:13 +0100 Subject: [PATCH 187/202] Update use ultraball with constraint (#3760) * Add Use Ultraball (#1) * Add `use_ultraball` event to Event Manager * Add use ultraball if pokeball + greatball = 0 * Add Use Ultraball if No Other Balls (#2) * Add `use_ultraball` event to Event Manager * Add use ultraball if pokeball + greatball = 0 * Add New Contributor * Revert "Add Use Ultraball" (#4) * Use Ultraball If No Other Balls (#3) * Add `use_ultraball` event to Event Manager * Add use ultraball if pokeball + greatball = 0 * Add New Contributor * Remove 'use_ultraball' event. * Remove `use_ultraball` event call * Update & add avoid catching Pokemon if no pokeball * Update conflict contributors * Add get `min_ultraball_to_keep` from config file * Improved `min_ultraball_to_keep` with condition * Added `min_ultraball_to_keep` option * Added `min_ultraball_to_keep` option * Added `min_ultraball_to_keep` option * Added `min_ultraball_to_keep` option * Add `min_ultraball_to_keep` option * Remove count all pokeballs * Resolved Conflicts * Change from `items_stock` to `ball_count` --- pokemongo_bot/cell_workers/pokemon_catch_worker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 1e5b161d13..4030a33e41 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -252,7 +252,7 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): ball_count[ball_id] = self.inventory.get(ball_id).count # use `min_ultraball_to_keep` from config if is not None - min_ultraball_to_keep = items_stock[ITEM_ULTRABALL] + min_ultraball_to_keep = ball_count[ITEM_ULTRABALL] if self.config.min_ultraball_to_keep is not None: if self.config.min_ultraball_to_keep >= 0 and self.config.min_ultraball_to_keep < min_ultraball_to_keep: min_ultraball_to_keep = self.config.min_ultraball_to_keep @@ -267,7 +267,7 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): self.emit_event('no_pokeballs', formatted='No usable pokeballs found!') # use untraball if there is no other balls with constraint to `min_ultraball_to_keep` - if maximum_ball != ITEM_ULTRABALL and items_stock[ITEM_ULTRABALL] > min_ultraball_to_keep: + if maximum_ball != ITEM_ULTRABALL and ball_count[ITEM_ULTRABALL] > min_ultraball_to_keep: maximum_ball = ITEM_ULTRABALL continue else: From a50ca97a9bea5e1160170a67630db068dfac97f3 Mon Sep 17 00:00:00 2001 From: Amal Samally Date: Fri, 12 Aug 2016 21:55:41 +0400 Subject: [PATCH 188/202] Rewrite NicknamePokemon for new Inventory sysem + a lot of new keys for 'nickname_template' option (#3756) * Add type information and classes - New classes: Type, Types - Usage of new classes anywhere in the Inventory - Tests coverage * Improve API for pokemons in Inventory - Added new class PokemonInfo for the static information loaded from json - API improved, added capture_rate and flee_rate - All covered with tests * Minor refactoring of inventory.py - Item class (moved to other instance classes) to keep file structure - Code style * Rewrite NicknamePokemon to use new Inventory sysem + add a lot of new keys for 'nickname_template' option + tests & documentation * Update documentaion for the new NicknamePokemon * Update documentaion for the new NicknamePokemon (again :) --- data/types.json | 247 +++++++ docs/configuration_files.md | 55 +- pokemongo_bot/__init__.py | 12 +- .../cell_workers/nickname_pokemon.py | 446 +++++++++++-- pokemongo_bot/inventory.py | 613 ++++++++++++------ tests/inventory_test.py | 110 ++-- tests/nickname_test.py | 91 +++ 7 files changed, 1258 insertions(+), 316 deletions(-) create mode 100644 data/types.json create mode 100644 tests/nickname_test.py diff --git a/data/types.json b/data/types.json new file mode 100644 index 0000000000..b4bad5e297 --- /dev/null +++ b/data/types.json @@ -0,0 +1,247 @@ +[ + { + "name": "Bug", + "effectiveAgainst": [ + "Dark", + "Grass", + "Psychic" + ], + "weakAgainst": [ + "Fairy", + "Fighting", + "Fire", + "Flying", + "Ghost", + "Poison", + "Steel" + ] + }, + { + "name": "Dark", + "effectiveAgainst": [ + "Ghost", + "Psychic" + ], + "weakAgainst": [ + "Dark", + "Fairy", + "Fighting" + ] + }, + { + "name": "Dragon", + "effectiveAgainst": [ + "Dragon" + ], + "weakAgainst": [ + "Fairy", + "Steel" + ] + }, + { + "name": "Electric", + "effectiveAgainst": [ + "Flying", + "Water" + ], + "weakAgainst": [ + "Dragon", + "Electric", + "Grass", + "Ground" + ] + }, + { + "name": "Fairy", + "effectiveAgainst": [ + "Dark", + "Dragon", + "Fighting" + ], + "weakAgainst": [ + "Fire", + "Poison", + "Steel" + ] + }, + { + "name": "Fighting", + "effectiveAgainst": [ + "Dark", + "Ice", + "Normal", + "Rock", + "Steel" + ], + "weakAgainst": [ + "Bug", + "Fairy", + "Flying", + "Ghost", + "Poison", + "Psychic" + ] + }, + { + "name": "Fire", + "effectiveAgainst": [ + "Bug", + "Grass", + "Ice", + "Steel" + ], + "weakAgainst": [ + "Dragon", + "Fire", + "Rock", + "Water" + ] + }, + { + "name": "Flying", + "effectiveAgainst": [ + "Bug", + "Fighting", + "Grass" + ], + "weakAgainst": [ + "Electric", + "Rock", + "Steel" + ] + }, + { + "name": "Ghost", + "effectiveAgainst": [ + "Ghost", + "Psychic" + ], + "weakAgainst": [ + "Dark", + "Normal" + ] + }, + { + "name": "Grass", + "effectiveAgainst": [ + "Ground", + "Rock", + "Water" + ], + "weakAgainst": [ + "Bug", + "Dragon", + "Fire", + "Flying", + "Grass", + "Poison", + "Steel" + ] + }, + { + "name": "Ground", + "effectiveAgainst": [ + "Electric", + "Fire", + "Poison", + "Rock", + "Steel" + ], + "weakAgainst": [ + "Bug", + "Flying", + "Grass" + ] + }, + { + "name": "Ice", + "effectiveAgainst": [ + "Dragon", + "Flying", + "Grass", + "Ground" + ], + "weakAgainst": [ + "Fire", + "Ice", + "Steel", + "Water" + ] + }, + { + "name": "Normal", + "effectiveAgainst": [], + "weakAgainst": [ + "Ghost", + "Rock", + "Steel" + ] + }, + { + "name": "Poison", + "effectiveAgainst": [ + "Fairy", + "Grass" + ], + "weakAgainst": [ + "Ghost", + "Ground", + "Poison", + "Rock", + "Steel" + ] + }, + { + "name": "Psychic", + "effectiveAgainst": [ + "Fighting", + "Poison" + ], + "weakAgainst": [ + "Dark", + "Psychic", + "Steel" + ] + }, + { + "name": "Rock", + "effectiveAgainst": [ + "Bug", + "Fire", + "Flying", + "Ice" + ], + "weakAgainst": [ + "Fighting", + "Ground", + "Steel" + ] + }, + { + "name": "Steel", + "effectiveAgainst": [ + "Fairy", + "Ice", + "Rock" + ], + "weakAgainst": [ + "Electric", + "Fire", + "Steel", + "Water" + ] + }, + { + "name": "Water", + "effectiveAgainst": [ + "Fire", + "Ground", + "Rock" + ], + "weakAgainst": [ + "Dragon", + "Grass", + "Water" + ] + } +] diff --git a/docs/configuration_files.md b/docs/configuration_files.md index ce9f5078a6..42b4feb069 100644 --- a/docs/configuration_files.md +++ b/docs/configuration_files.md @@ -40,6 +40,8 @@ The behaviors of the bot are configured via the `tasks` key in the `config.json` * [MoveToMapPokemon](#sniping-movetolocation) * NicknamePokemon * `nickname_template`: Default `""` | See the [Pokemon Nicknaming](#pokemon-nicknaming) section for more details + * `dont_nickname_favorite`: Default `false` | Prevents renaming of favorited pokemons + * `good_attack_threshold`: Default `0.7` | Threshold for perfection of the attack in it's type *(0.0-1.0)* after which attack will be treated as good.
Used for `{fast_attack_char}`, `{charged_attack_char}`, `{attack_code}` templates * RecycleItems * `item_filter`: Pass a list of unwanted [items (using their JSON codes)](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Item-ID's) to recycle when collected at a Pokestop * SpinFort @@ -202,15 +204,49 @@ Niantic imposes a 12-character limit on all pokemon nicknames, so any new nickna Because some pokemon have very long names, you can use the [Format String syntax](https://docs.python.org/2.7/library/string.html#formatstrings) to ensure that your names do not cause your templates to truncate. For example, using `{name:.8s}` causes the Pokemon name to never take up more than 8 characters in the nickname. This would help guarantee that a template like `{name:.8s}_{iv_pct}` never goes over the 12-character limit. Valid names in templates are: -- `name` = pokemon name -- `id` = pokemon type id (e.g. 1 for Bulbasaurs) -- `cp` = pokemon's CP -- `iv_attack` = pokemon's attack IV -- `iv_defense` = pokemon's defense IV -- `iv_stamina` = pokemon's stamina IV -- `iv_ads` = pokemon's IVs in `(attack)/(defense)/(stamina)` format (matches web UI format -- A/D/S) -- `iv_sum` = pokemon's IVs as a sum (e.g. 45 when 3 perfect 15 IVs) -- `iv_pct` = pokemon's IVs as a percentage (0-100) + +Key | Info +---- | ---- +**{name}** | Pokemon name *(e.g. Articuno)* +**{id}** | Pokemon ID/Number *(1-151, e.g. 1 for Bulbasaurs)* +**{cp}** | Pokemon's Combat Points (CP) *(10-4145)* + | **Individial Values (IV)** +**{iv_attack}** | Individial Attack *(0-15)* of the current specific pokemon +**{iv_defense}** | Individial Defense *(0-15)* of the current specific pokemon +**{iv_stamina}** | Individial Stamina *(0-15)* of the current specific pokemon +**{iv_ads}** | Joined IV values in `(attack)/(defense)/(stamina)` format (*e.g. 4/12/9*, matches web UI format -- A/D/S) +**{iv_sum}** | Sum of the Individial Values *(0-45, e.g. 45 when 3 perfect 15 IVs)* + | **Basic Values of the pokemon (identical for all of one kind)** +**{base_attack}** | Basic Attack *(40-284)* of the current pokemon kind +**{base_defense}** | Basic Defense *(54-242)* of the current pokemon kind +**{base_stamina}** | Basic Stamina *(20-500)* of the current pokemon kind +**{base_ads}** | Joined Basic Values *(e.g. 125/93/314)* + | **Final Values of the pokemon (Base Values + Individial Values)** +**{attack}** | Basic Attack + Individial Attack +**{defense}** | Basic Defense + Individial Defense +**{stamina}** | Basic Stamina + Individial Stamina +**{sum_ads}** | Joined Final Values *(e.g. 129/97/321)* + | **Individial Values perfection percent** +**{iv_pct}** | IV perfection *(in 000-100 format - 3 chars)* +**{iv_pct2}** | IV perfection *(in 00-99 format - 2 chars).* So 99 is best (it's a 100% perfection) +**{iv_pct1}** | IV perfection *(in 0-9 format - 1 char)* + | **IV CP perfection - kind of IV perfection percent but calculated using weight of each IV in its contribution to CP of the best evolution of current pokemon.**
It tends to be more accurate than simple IV perfection. +**{ivcp_pct}** | IV CP perfection *(in 000-100 format - 3 chars)* +**{ivcp_pct2}** | IV CP perfection *(in 00-99 format - 2 chars).* So 99 is best (it's a 100% perfection) +**{ivcp_pct1}** | IV CP perfection *(in 0-9 format - 1 char)* + | **Moveset perfection percents for attack and for defense.**
Calculated for current pokemon only, not between all pokemons. So perfect moveset can be weak if pokemon is weak (e.g. Caterpie) +**{attack_pct}** | Moveset perfection for attack *(in 000-100 format - 3 chars)* +**{attack_pct2}** | Moveset perfection for attack *(in 00-99 format - 2 chars)* +**{attack_pct1}** | Moveset perfection for attack *(in 0-9 format - 1 char)* +**{defense_pct}** | Moveset perfection for defense *(in 000-100 format - 3 chars)* +**{defense_pct2}** | Moveset perfection for defense *(in 00-99 format - 2 chars)* +**{defense_pct1}** | Moveset perfection for defense *(in 0-9 format - 1 char)* + | **Character codes for fast/charged attack types.**
If attack is good character is uppecased, otherwise lowercased.
Use `'good_attack_threshold'` option for customization.

It's an effective way to represent type with one character.
If first char of the type name is unique - it's used, in other case suitable substitute used.

Type codes:
  `Bug: 'B'`
  `Dark: 'K'`
  `Dragon: 'D'`
  `Electric: 'E'`
  `Fairy: 'Y'`
  `Fighting: 'T'`
  `Fire: 'F'`
  `Flying: 'L'`
  `Ghost: 'H'`
  `Grass: 'A'`
  `Ground: 'G'`
  `Ice: 'I'`
  `Normal: 'N'`
  `Poison: 'P'`
  `Psychic: 'C'`
  `Rock: 'R'`
  `Steel: 'S'`
  `Water: 'W'` +**{fast_attack_char}** | One character code for fast attack type (e.g. 'F' for good Fire or 's' for bad Steel attack) +**{charged_attack_char}** | One character code for charged attack type (e.g. 'n' for bad Normal or 'I' for good Ice attack) +**{attack_code}** | Joined 2 character code for both attacks (e.g. 'Lh' for pokemon with strong Flying and weak Ghost attacks) + | **Special case: pokemon object**
You can access any available pokemon info via it.
Examples:
  `'{pokemon.ivcp:.2%}' -> '47.00%'`
  `'{pokemon.fast_attack}' -> 'Wing Attack'`
  `'{pokemon.fast_attack.type}' -> 'Flying'`
  `'{pokemon.fast_attack.dps:.2f}' -> '10.91'`
  `'{pokemon.fast_attack.dps:.0f}' -> '11'`
  `'{pokemon.charged_attack}' -> 'Ominous Wind'` +**{pokemon}** | Pokemon instance (see inventory.py for class sources) > **NOTE:** Use a blank template (`""`) to revert all pokemon to their original names (as if they had no nickname). @@ -218,6 +254,7 @@ Sample usages: - `"{name}_{iv_pct}"` => `Mankey_69` - `"{iv_pct}_{iv_ads}"` => `91_15/11/15` - `""` -> `Mankey` +- `"{attack_code}{attack_pct1}{defense_pct1}{ivcp_pct1}{name}"` => `Lh474Golbat` ![sample](https://cloud.githubusercontent.com/assets/8896778/17285954/0fa44a88-577b-11e6-8204-b1302f4294bd.png) ## Sniping _(MoveToLocation)_ diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index dcf83bdc84..88fd9916b4 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -418,15 +418,16 @@ def _register_events(self): # rename self.event_manager.register_event( 'rename_pokemon', - parameters=( - 'old_name', 'current_name' - ) + parameters=('old_name', 'current_name',) ) self.event_manager.register_event( 'pokemon_nickname_invalid', parameters=('nickname',) ) - self.event_manager.register_event('unset_pokemon_nickname') + self.event_manager.register_event( + 'unset_pokemon_nickname', + parameters=('old_name',) + ) # Move To map pokemon self.event_manager.register_event( @@ -763,7 +764,8 @@ def _print_character_info(self): self.logger.info( 'PokeBalls: ' + str(items_stock[1]) + ' | GreatBalls: ' + str(items_stock[2]) + - ' | UltraBalls: ' + str(items_stock[3])) + ' | UltraBalls: ' + str(items_stock[3]) + + ' | MasterBalls: ' + str(items_stock[4])) self.logger.info( 'RazzBerries: ' + str(items_stock[701]) + diff --git a/pokemongo_bot/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py index 68943e7a8e..493d2656ff 100644 --- a/pokemongo_bot/cell_workers/nickname_pokemon.py +++ b/pokemongo_bot/cell_workers/nickname_pokemon.py @@ -1,103 +1,427 @@ -from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.inventory import pokemons, Pokemon, Attack + + +DEFAULT_IGNORE_FAVORITES = False +DEFAULT_GOOD_ATTACK_THRESHOLD = 0.7 +DEFAULT_TEMPLATE = '{name}' + +MAXIMUM_NICKNAME_LENGTH = 12 + class NicknamePokemon(BaseTask): SUPPORTED_TASK_API_VERSION = 1 + """ + Nickname user pokemons according to the specified template + + + PARAMETERS: + + dont_nickname_favorite (default: False) + Prevents renaming of favorited pokemons + + good_attack_threshold (default: 0.7) + Threshold for perfection of the attack in it's type (0.0-1.0) + after which attack will be treated as good. + Used for {fast_attack_char}, {charged_attack_char}, {attack_code} + templates + + nickname_template (default: '{name}') + Template for nickname generation. + Empty template or any resulting in the simple pokemon name + (e.g. '', '{name}', ...) will revert all pokemon to their original + names (as if they had no nickname). + + Niantic imposes a 12-character limit on all pokemon nicknames, so + any new nickname will be truncated to 12 characters if over that limit. + Thus, it is up to the user to exercise judgment on what template will + best suit their need with this constraint in mind. + + You can use full force of the Python [Format String syntax](https://docs.python.org/2.7/library/string.html#formatstrings) + For example, using `{name:.8s}` causes the Pokemon name to never take up + more than 8 characters in the nickname. This would help guarantee that + a template like `{name:.8s}_{iv_pct}` never goes over the 12-character + limit. + + + **NOTE:** If you experience frequent `Pokemon not found` error messages, + this is because the inventory cache has not been updated after a pokemon + was released. This can be remedied by placing the `NicknamePokemon` task + above the `TransferPokemon` task in your `config.json` file. + + + EXAMPLE CONFIG: + { + "type": "NicknamePokemon", + "config": { + "enabled": true, + "dont_nickname_favorite": false, + "good_attack_threshold": 0.7, + "nickname_template": "{iv_pct}_{iv_ads}" + } + } + + + SUPPORTED PATTERN KEYS: + + {name} Pokemon name (e.g. Articuno) + {id} Pokemon ID/Number (1-151) + {cp} Combat Points (10-4145) + + # Individial Values + {iv_attack} Individial Attack (0-15) of the current specific pokemon + {iv_defense} Individial Defense (0-15) of the current specific pokemon + {iv_stamina} Individial Stamina (0-15) of the current specific pokemon + {iv_ads} Joined IV values (e.g. 4/12/9) + {iv_sum} Sum of the Individial Values (0-45) + {iv_pct} IV perfection (in 000-100 format - 3 chars) + {iv_pct2} IV perfection (in 00-99 format - 2 chars) + So 99 is best (it's a 100% perfection) + {iv_pct1} IV perfection (in 0-9 format - 1 char) + + # Basic Values of the pokemon (identical for all of one kind) + {base_attack} Basic Attack (40-284) of the current pokemon kind + {base_defense} Basic Defense (54-242) of the current pokemon kind + {base_stamina} Basic Stamina (20-500) of the current pokemon kind + {base_ads} Joined Basic Values (e.g. 125/93/314) + + # Final Values of the pokemon (Base Values + Individial Values) + {attack} Basic Attack + Individial Attack + {defense} Basic Defense + Individial Defense + {stamina} Basic Stamina + Individial Stamina + {sum_ads} Joined Final Values (e.g. 129/97/321) + + # IV CP perfection - it's a kind of IV perfection percent + # but calculated using weight of each IV in its contribution + # to CP of the best evolution of current pokemon. + # So it tends to be more accurate than simple IV perfection. + {ivcp_pct} IV CP perfection (in 000-100 format - 3 chars) + {ivcp_pct2} IV CP perfection (in 00-99 format - 2 chars) + So 99 is best (it's a 100% perfection) + {ivcp_pct1} IV CP perfection (in 0-9 format - 1 char) + + # Character codes for fast/charged attack types. + # If attack is good character is uppecased, otherwise lowercased. + # Use 'good_attack_threshold' option for customization + # + # It's an effective way to represent type with one character. + # If first char of the type name is unique - use it, + # in other case suitable substitute used + # + # Type codes: + # Bug: 'B' + # Dark: 'K' + # Dragon: 'D' + # Electric: 'E' + # Fairy: 'Y' + # Fighting: 'T' + # Fire: 'F' + # Flying: 'L' + # Ghost: 'H' + # Grass: 'A' + # Ground: 'G' + # Ice: 'I' + # Normal: 'N' + # Poison: 'P' + # Psychic: 'C' + # Rock: 'R' + # Steel: 'S' + # Water: 'W' + # + {fast_attack_char} One character code for fast attack type + (e.g. 'F' for good Fire or 's' for bad + Steel attack) + {charged_attack_char} One character code for charged attack type + (e.g. 'n' for bad Normal or 'I' for good + Ice attack) + {attack_code} Joined 2 character code for both attacks + (e.g. 'Lh' for pokemon with good Flying + and weak Ghost attacks) + + # Moveset perfection percents for attack and for defense + # Calculated for current pokemon only, not between all pokemons + # So perfect moveset can be weak if pokemon is weak (e.g. Caterpie) + {attack_pct} Moveset perfection for attack (in 000-100 format - 3 chars) + {defense_pct} Moveset perfection for defense (in 000-100 format - 3 chars) + {attack_pct2} Moveset perfection for attack (in 00-99 format - 2 chars) + {defense_pct2} Moveset perfection for defense (in 00-99 format - 2 chars) + {attack_pct1} Moveset perfection for attack (in 0-9 format - 1 char) + {defense_pct1} Moveset perfection for defense (in 0-9 format - 1 char) + + # Special case: pokemon object. + # You can access any available pokemon info via it. + # Examples: + # '{pokemon.ivcp:.2%}' -> '47.00%' + # '{pokemon.fast_attack}' -> 'Wing Attack' + # '{pokemon.fast_attack.type}' -> 'Flying' + # '{pokemon.fast_attack.dps:.2f}' -> '10.91' + # '{pokemon.fast_attack.dps:.0f}' -> '11' + # '{pokemon.charged_attack}' -> 'Ominous Wind' + {pokemon} Pokemon instance (see inventory.py for class sources) + + + EXAMPLES: + + 1. "nickname_template": "{ivcp_pct}_{iv_pct}_{iv_ads}" + + Golbat with IV (attack: 9, defense: 4 and stamina: 8) will result in: + '48_46_9/4/8' + + 2. "nickname_template": "{attack_code}{attack_pct1}{defense_pct1}{ivcp_pct1}{name}" + + Same Golbat (with attacks Wing Attack & Ominous Wind) will have nickname: + 'Lh474Golbat' + + See /tests/nickname_test.py for more examples. + """ + + # noinspection PyAttributeOutsideInit def initialize(self): - self.template = self.config.get('nickname_template','').lower().strip() - if self.template == "{name}": - self.template = "" + self.ignore_favorites = self.config.get( + 'dont_nickname_favorite', DEFAULT_IGNORE_FAVORITES) + self.good_attack_threshold = self.config.get( + 'good_attack_threshold', DEFAULT_GOOD_ATTACK_THRESHOLD) + self.template = self.config.get( + 'nickname_template', DEFAULT_TEMPLATE) def work(self): - try: - inventory = reduce(dict.__getitem__, ["responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], self.bot.get_inventory()) - except KeyError: - pass - else: - pokemon_data = self._get_inventory_pokemon(inventory) - for pokemon in pokemon_data: + """ + Iterate over all user pokemons and nickname if needed + """ + for pokemon in pokemons().all(): # type: Pokemon + if not pokemon.is_favorite or not self.ignore_favorites: self._nickname_pokemon(pokemon) - def _get_inventory_pokemon(self,inventory_dict): - pokemon_data = [] - for inv_data in inventory_dict: - try: - pokemon = reduce(dict.__getitem__,['inventory_item_data','pokemon_data'],inv_data) - except KeyError: - pass - else: - if not pokemon.get('is_egg',False) and not (pokemon.get('favorite', 0) == 1 and self.config.get('dont_nickname_favorite',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 = "" - instance_id = pokemon.get('id',0) + def _nickname_pokemon(self, pokemon): + # type: (Pokemon) -> None + """ + Nicknaming process + """ + + # We need id of the specific pokemon unstance to be able to rename it + instance_id = pokemon.id if not instance_id: self.emit_event( 'api_error', formatted='Failed to get pokemon name, will not rename.' ) return - id = pokemon.get('pokemon_id',0)-1 - name = self.bot.pokemon_list[id]['Name'] - cp = pokemon.get('cp',0) - iv_attack = pokemon.get('individual_attack',0) - iv_defense = pokemon.get('individual_defense',0) - iv_stamina = pokemon.get('individual_stamina',0) - iv_list = [iv_attack,iv_defense,iv_stamina] - iv_ads = "/".join(map(str,iv_list)) - iv_sum = sum(iv_list) - iv_pct = "{:03.0f}".format(100*iv_sum/45.0) - log_color = 'red' + + # Generate new nickname + old_nickname = pokemon.nickname try: - new_name = self.template.format(name=name, - id=id, - cp=cp, - iv_attack=iv_attack, - iv_defense=iv_defense, - iv_stamina=iv_stamina, - iv_ads=iv_ads, - iv_sum=iv_sum, - iv_pct=iv_pct)[:12] + new_nickname = self._generate_new_nickname(pokemon, self.template) except KeyError as bad_key: self.emit_event( 'config_error', - formatted="Unable to nickname {} due to bad template ({})".format(name,bad_key) + formatted="Unable to nickname {} due to bad template ({})" + .format(old_nickname, bad_key) ) - if pokemon.get('nickname', '') == new_name: return - response = self.bot.api.nickname_pokemon(pokemon_id=instance_id,nickname=new_name) - sleep(1.2) + + # Skip if pokemon is already well named + if pokemon.nickname_raw == new_nickname: + return + + # Send request + response = self.bot.api.nickname_pokemon( + pokemon_id=instance_id, nickname=new_nickname) + sleep(1.2) # wait a bit after request + + # Check result try: - result = reduce(dict.__getitem__, ["responses", "NICKNAME_POKEMON"], response) + result = reduce(dict.__getitem__, ["responses", "NICKNAME_POKEMON"], + response)['result'] except KeyError: self.emit_event( 'api_error', formatted='Attempt to nickname received bad response from server.' ) - result = result['result'] - new_name = new_name or name + return + + # Nickname unset if result == 0: self.emit_event( 'unset_pokemon_nickname', - formatted="Pokemon nickname unset." + formatted="Pokemon {old_name} nickname unset.", + data={'old_name': old_nickname} ) + pokemon.update_nickname(new_nickname) elif result == 1: self.emit_event( 'rename_pokemon', formatted="Pokemon {old_name} renamed to {current_name}", - data={ - 'old_name': name, - 'current_name': new_name - } + data={'old_name': old_nickname, 'current_name': new_nickname} ) - pokemon['nickname'] = new_name + pokemon.update_nickname(new_nickname) elif result == 2: self.emit_event( 'pokemon_nickname_invalid', formatted="Nickname {nickname} is invalid", - data={'nickname': new_name} + data={'nickname': new_nickname} ) + else: + self.emit_event( + 'api_error', + formatted='Attempt to nickname received unexpected result' + ' from server ({}).'.format(result) + ) + + def _generate_new_nickname(self, pokemon, template): + # type: (Pokemon, string) -> string + """ + New nickname generation + """ + + # Filter template + template = template.lower().strip() + + # Individial Values of the current specific pokemon (different for each) + iv_attack = pokemon.iv_attack + iv_defense = pokemon.iv_defense + iv_stamina = pokemon.iv_stamina + iv_list = [iv_attack, iv_defense, iv_stamina] + iv_sum = sum(iv_list) + iv_pct = iv_sum / 45.0 + + # Basic Values of the pokemon (identical for all of one kind) + base_attack = pokemon.static.base_attack + base_defense = pokemon.static.base_defense + base_stamina = pokemon.static.base_stamina + + # Final Values of the pokemon + attack = base_attack + iv_attack + defense = base_defense + iv_defense + stamina = base_stamina + iv_stamina + + # One character codes for fast/charged attack types + # If attack is good then character is uppecased, otherwise lowercased + fast_attack_char = self.attack_char(pokemon.fast_attack) + charged_attack_char = self.attack_char(pokemon.charged_attack) + # 2 characters code for both attacks of the pokemon + attack_code = fast_attack_char + charged_attack_char + + moveset = pokemon.moveset + + # + # Generate new nickname + # + new_name = template.format( + # Pokemon + pokemon=pokemon, + # Pokemon name + name=pokemon.name, + # Pokemon ID/Number + id=int(pokemon.pokemon_id), + # Combat Points + cp=int(pokemon.cp), + + # Individial Values of the current specific pokemon + iv_attack=iv_attack, + iv_defense=iv_defense, + iv_stamina=iv_stamina, + # Joined IV values like: 4/12/9 + iv_ads='/'.join(map(str, iv_list)), + # Sum of the Individial Values + iv_sum=iv_sum, + # IV perfection (in 000-100 format - 3 chars) + iv_pct="{:03.0f}".format(iv_pct * 100), + # IV perfection (in 00-99 format - 2 chars) + # 99 is best (it's a 100% perfection) + iv_pct2="{:02.0f}".format(iv_pct * 99), + # IV perfection (in 0-9 format - 1 char) + # 9 is best (it's a 100% perfection) + iv_pct1=int(round(iv_pct * 9)), + + # Basic Values of the pokemon (identical for all of one kind) + base_attack=base_attack, + base_defense=base_defense, + base_stamina=base_stamina, + # Joined Base Values like: 125/93/314 + base_ads='/'.join(map(str, [base_attack, base_defense, base_stamina])), + + # Final Values of the pokemon (Base Values + Individial Values) + attack=attack, + defense=defense, + stamina=stamina, + # Joined Final Values like: 129/97/321 + sum_ads='/'.join(map(str, [attack, defense, stamina])), + + # IV CP perfection (in 000-100 format - 3 chars) + # It's a kind of IV perfection percent but calculated + # using weight of each IV in its contribution to CP of the best + # evolution of current pokemon + # So it tends to be more accurate than simple IV perfection + ivcp_pct="{:03.0f}".format(pokemon.ivcp * 100), + # IV CP perfection (in 00-99 format - 2 chars) + ivcp_pct2="{:02.0f}".format(pokemon.ivcp * 99), + # IV CP perfection (in 0-9 format - 1 char) + ivcp_pct1=int(round(pokemon.ivcp * 9)), + + # One character code for fast attack type + # If attack is good character is uppecased, otherwise lowercased + fast_attack_char=fast_attack_char, + # One character code for charged attack type + charged_attack_char=charged_attack_char, + # 2 characters code for both attacks of the pokemon + attack_code=attack_code, + + # Moveset perfection for attack and for defense (in 000-100 format) + # Calculated for current pokemon only, not between all pokemons + # So perfect moveset can be weak if pokemon is weak (e.g. Caterpie) + attack_pct="{:03.0f}".format(moveset.attack_perfection * 100), + defense_pct="{:03.0f}".format(moveset.defense_perfection * 100), + + # Moveset perfection (in 00-99 format - 2 chars) + attack_pct2="{:02.0f}".format(moveset.attack_perfection * 99), + defense_pct2="{:02.0f}".format(moveset.defense_perfection * 99), + + # Moveset perfection (in 0-9 format - 1 char) + attack_pct1=int(round(moveset.attack_perfection * 9)), + defense_pct1=int(round(moveset.defense_perfection * 9)), + ) + + # Use empty result for unsetting nickname + # So original pokemon name will be shown to user + if new_name == pokemon.name: + new_name = '' + + # 12 is a max allowed length for the nickname + return new_name[:MAXIMUM_NICKNAME_LENGTH] + + def attack_char(self, attack): + # type: (Attack) -> string + """ + One character code for attack type + If attack is good then character is uppecased, otherwise lowercased + + Type codes: + + Bug: 'B' + Dark: 'K' + Dragon: 'D' + Electric: 'E' + Fairy: 'Y' + Fighting: 'T' + Fire: 'F' + Flying: 'L' + Ghost: 'H' + Grass: 'A' + Ground: 'G' + Ice: 'I' + Normal: 'N' + Poison: 'P' + Psychic: 'C' + Rock: 'R' + Steel: 'S' + Water: 'W' + + it's an effective way to represent type with one character + if first char is unique - use it, in other case suitable substitute used + """ + char = attack.type.as_one_char.upper() + if attack.rate_in_type < self.good_attack_threshold: + char = char.lower() + return char diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index c7c3dfafcd..f314687a13 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1,10 +1,16 @@ import json import logging import os +from collections import OrderedDict + from pokemongo_bot.base_dir import _base_dir ''' Helper class for updating/retrieving Inventory data + +Interesting info and formulas: +https://drive.google.com/file/d/0B0TeYGBPiuzaenhUNE5UWnRCVlU/view +https://www.reddit.com/r/pokemongodev/comments/4w7mdg/combat_damage_calculation_formula_exactly/ ''' @@ -100,22 +106,6 @@ 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' @@ -134,7 +124,8 @@ def get(self, item_id): def name_for(cls, item_id): return cls.STATIC_DATA[str(item_id)] - def get_space_used(self): + @classmethod + def get_space_used(cls): """ Counts the space used in item inventory. :return: The space used in item inventory. @@ -145,14 +136,15 @@ def get_space_used(self): space_used += item_in_inventory.count return space_used - def get_space_left(self): + @classmethod + def get_space_left(cls): """ 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() + return _inventory.item_inventory_size - cls.get_space_used() class Pokemons(_BaseInventoryComponent): @@ -162,174 +154,57 @@ class Pokemons(_BaseInventoryComponent): @classmethod def process_static_data(cls, data): - pokemon_id = 1 - for poke_info in data: - # prepare types - types = [poke_info['Type I'][0]] # required - for t in poke_info.get('Type II', []): - types.append(t) - poke_info['types'] = types - - # prepare attacks (moves) - cls._process_attacks(poke_info) - cls._process_attacks(poke_info, charged=True) - - # prepare movesets - poke_info['movesets'] = cls._process_movesets(poke_info, pokemon_id) - - # calculate maximum CP for the pokemon (best IVs, lvl 40) - base_attack = poke_info['BaseAttack'] - base_defense = poke_info['BaseDefense'] - base_stamina = poke_info['BaseStamina'] - max_cp = _calc_cp(base_attack, base_defense, base_stamina) - poke_info['max_cp'] = max_cp - - pokemon_id += 1 - return data + data = [PokemonInfo(d) for d in data] - @classmethod - def _process_movesets(cls, poke_info, pokemon_id): - # type: (dict, int) -> List[Moveset] - """ - The optimal moveset is the combination of two moves, one quick move - and one charge move, that deals the most damage over time. + # process evolution info + for p in data: + next_all = p.next_evolutions_all + if len(next_all) <= 0: + continue - Because each quick move gains a certain amount of energy (different - for different moves) and each charge move requires a different amount - of energy to use, sometimes, a quick move with lower DPS will be - better since it charges the charge move faster. On the same note, - sometimes a charge move that has lower DPS will be more optimal since - it may require less energy or it may last for a longer period of time. + # only next level evolutions, not all possible + p.next_evolution_ids = [idx for idx in next_all + if data[idx-1].prev_evolution_id == p.id] - Attacker have STAB (Same-type attack bonus - x1.25) pokemon have the - same type as attack. So we add it to the "Combo DPS" of the moveset. - - The defender attacks in intervals of 1 second for the first 2 attacks, - and then in intervals of 2 seconds for the remainder of the attacks. - This explains why we see two consecutive quick attacks at the beginning - of the match. As a result, we add +2 seconds to the DPS calculation - for defender DPS output. - - So to determine an optimal defensive moveset, we follow the same method - as we did for optimal offensive movesets, but instead calculate the - highest "Combo DPS" with an added 2 seconds to the quick move cool down. - - Note: critical hits have not yet been implemented in the game - - See http://pokemongo.gamepress.gg/optimal-moveset-explanation - See http://pokemongo.gamepress.gg/defensive-tactics - """ - - # Prepare movesets - movesets = [] - types = poke_info['types'] - for fm in poke_info['Fast Attack(s)']: - for chm in poke_info['Special Attack(s)']: - movesets.append(Moveset(fm, chm, types, pokemon_id)) - assert len(movesets) > 0 + # only final evolutions + p.last_evolution_ids = [idx for idx in next_all + if not data[idx-1].has_next_evolution] + assert len(p.last_evolution_ids) > 0 - # Calculate attack perfection for each moveset - movesets = sorted(movesets, key=lambda m: m.dps_attack) - worst_dps = movesets[0].dps_attack - best_dps = movesets[-1].dps_attack - if best_dps > worst_dps: - for moveset in movesets: - current_dps = moveset.dps_attack - moveset.attack_perfection = \ - (current_dps - worst_dps) / (best_dps - worst_dps) - - # Calculate defense perfection for each moveset - movesets = sorted(movesets, key=lambda m: m.dps_defense) - worst_dps = movesets[0].dps_defense - best_dps = movesets[-1].dps_defense - if best_dps > worst_dps: - for moveset in movesets: - current_dps = moveset.dps_defense - moveset.defense_perfection = \ - (current_dps - worst_dps) / (best_dps - worst_dps) - - return sorted(movesets, key=lambda m: m.dps, reverse=True) - - @classmethod - def _process_attacks(cls, poke_info, charged=False): - # type: (dict, bool) -> List[Attack] - key = 'Fast Attack(s)' if not charged else 'Special Attack(s)' - moves_dict = (ChargedAttacks if charged else FastAttacks).BY_NAME - moves = [] - for name in poke_info[key]: - if name not in moves_dict: - raise KeyError('Unknown {} attack: "{}"'.format( - 'charged' if charged else 'fast', name)) - moves.append(moves_dict[name]) - moves = sorted(moves, key=lambda m: m.dps, reverse=True) - poke_info[key] = moves - assert len(moves) > 0 - return moves + return data @classmethod def data_for(cls, pokemon_id): - # type: (int) -> dict + # type: (int) -> PokemonInfo return cls.STATIC_DATA[pokemon_id - 1] @classmethod def name_for(cls, pokemon_id): - # type: (int) -> string - return cls.data_for(pokemon_id)['Name'] + 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 + return cls.data_for(pokemon_id).first_evolution_id @classmethod def prev_evolution_id_for(cls, pokemon_id): - data = cls.data_for(pokemon_id) - if 'Previous evolution(s)' in data: - return int(data['Previous evolution(s)'][-1]['Number']) - return None + return cls.data_for(pokemon_id).prev_evolution_id @classmethod def next_evolution_ids_for(cls, pokemon_id): - try: - next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] - except KeyError: - return [] - # get only next level evolutions, not all possible - ids = [] - for p in next_evolutions: - p_id = int(p['Number']) - if cls.prev_evolution_id_for(p_id) == pokemon_id: - ids.append(p_id) - return ids + return cls.data_for(pokemon_id).next_evolution_ids @classmethod def last_evolution_ids_for(cls, pokemon_id): - try: - next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] - except KeyError: - return [pokemon_id] - # get only final evolutions, not all possible - ids = [] - for p in next_evolutions: - p_id = int(p['Number']) - if len(cls.data_for(p_id).get('Next evolution(s)', [])) == 0: - ids.append(p_id) - assert len(ids) > 0 - return ids + return cls.data_for(pokemon_id).last_evolution_ids @classmethod def has_next_evolution(cls, pokemon_id): - poke_info = cls.data_for(pokemon_id) - return 'Next Evolution Requirements' in poke_info \ - or 'Next evolution(s)' in poke_info + return cls.data_for(pokemon_id).has_next_evolution @classmethod def evolution_cost_for(cls, pokemon_id): - if not cls.has_next_evolution(pokemon_id): - return None - return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + return cls.data_for(pokemon_id).evolution_cost def parse(self, item): if 'is_egg' in item: @@ -357,6 +232,71 @@ def remove(self, pokemon_id): # # Static Components +class Types(_StaticInventoryComponent): + """ + Types of attacks and pokemons + + See more information: + https://i.redd.it/oy7lrixl8r9x.png + https://www.reddit.com/r/TheSilphRoad/comments/4t8seh/pokemon_go_type_advantage_chart/ + https://github.com/jehy/Pokemon-Go-Weakness-calculator/blob/master/app/src/main/java/ru/jehy/pokemonweaknesscalculator/MainActivity.java#L31 + """ + + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'types.json') + + @classmethod + def process_static_data(cls, data): + # create instances + ret = OrderedDict() + for t in sorted(data, key=lambda x: x["name"]): + name = str(t["name"]) + ret[name] = Type(name, t["effectiveAgainst"], t["weakAgainst"]) + + # additional manipulations + size = len(ret) + by_effectiveness = {} + by_resistance = {} + for t in ret.itervalues(): # type: Type + t.attack_effective_against = [ret[name] for name in t.attack_effective_against] + t.attack_weak_against = [ret[name] for name in t.attack_weak_against] + + # group types effective against, weak against specific types + for l, d in (t.attack_effective_against, by_effectiveness), \ + (t.attack_weak_against, by_resistance): + for tt in l: + if tt not in d: + d[tt] = set() + d[tt].add(t) + + # calc average factor for damage of this type relative to all types + t.rate = (size + + ((EFFECTIVENESS_FACTOR-1) * len(t.attack_effective_against)) + - ((1-RESISTANCE_FACTOR) * len(t.attack_weak_against))) / size + + # set pokemon type resistance/weakness info + for t in ret.itervalues(): # type: Type + t.pokemon_resistant_to = by_resistance[t] + t.pokemon_vulnerable_to = by_effectiveness[t] + + return ret + + @classmethod + def get(cls, type_name): + # type: (Union[string, Type]) -> Type + type_name = str(type_name) + if type_name not in cls.STATIC_DATA: + raise ValueError("Unknown type: {}".format(type_name)) + return cls.STATIC_DATA[type_name] + + @classmethod + def all(cls): + return cls.STATIC_DATA.values() + + @classmethod + def rating(cls): + return sorted(cls.all(), key=lambda x: x.rate, reverse=True) + + class LevelToCPm(_StaticInventoryComponent): """ Data for the CP multipliers at different levels @@ -374,6 +314,7 @@ class LevelToCPm(_StaticInventoryComponent): def init_static_data(cls): super(LevelToCPm, cls).init_static_data() cls.MAX_CPM = cls.cp_multiplier_for(cls.MAX_LEVEL) + assert cls.MAX_CPM > .0 @classmethod def cp_multiplier_for(cls, level): @@ -408,9 +349,10 @@ def process_static_data(cls, moves): ret[attack.id] = attack by_name[attack.name] = attack - if attack.type not in by_type: - by_type[attack.type] = [] - by_type[attack.type].append(attack) + attack_type = str(attack.type) + if attack_type not in by_type: + by_type[attack_type] = [] + by_type[attack_type].append(attack) for t in by_type.iterkeys(): attacks = sorted(by_type[t], key=lambda m: m.dps, reverse=True) @@ -442,11 +384,11 @@ def by_name(cls, name): @classmethod def list_for_type(cls, type_name): - # type: (string) -> List[Attack] + # type: (Union[string, Type]) -> List[Attack] """ :return: Attacks sorted by DPS in descending order """ - return cls.BY_TYPE[type_name] + return cls.BY_TYPE[str(type_name)] @classmethod def all(cls): @@ -468,6 +410,59 @@ class ChargedAttacks(_Attacks): # # Instances +class Type(object): + def __init__(self, name, effective_against, weak_against): + # type: (string, Iterable[Type], Iterable[Type]) -> None + + self.name = name + + # effective way to represent type with one character + # for example it's very useful for nicknaming pokemon + # using its attack types + # + # if first char is unique - use it, in other case + # use suitable substitute + type_to_one_char_map = { + 'Bug': 'B', + 'Dark': 'K', + 'Dragon': 'D', + 'Electric': 'E', + 'Fairy': 'Y', + 'Fighting': 'T', + 'Fire': 'F', + 'Flying': 'L', + 'Ghost': 'H', + 'Grass': 'A', + 'Ground': 'G', + 'Ice': 'I', + 'Normal': 'N', + 'Poison': 'P', + 'Psychic': 'C', + 'Rock': 'R', + 'Steel': 'S', + 'Water': 'W', + } + self.as_one_char = type_to_one_char_map[name] + + # attack of this type is effective against ... + self.attack_effective_against = set(effective_against) + # attack of this type is weak against ... + self.attack_weak_against = set(weak_against) + # pokemon of this type is resistant to ... + self.pokemon_resistant_to = set() # type: Set[Type] + # pokemon of this type is vulnerable to ... + self.pokemon_vulnerable_to = set() # type: Set[Type] + + # average factor for damage of this type relative to all types + self.rate = 1. + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + + class Candy(object): def __init__(self, family_id, quantity): self.type = Pokemons.name_for(family_id) @@ -484,6 +479,23 @@ def add(self, amount): self.quantity += amount +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 Egg(object): def __init__(self, data): self._data = data @@ -492,6 +504,165 @@ def has_next_evolution(self): return False +class PokemonInfo(object): + """ + Static information about pokemon kind + """ + def __init__(self, data): + self._data = data + self.id = int(data["Number"]) + self.name = data['Name'] # type: string + self.classification = data['Classification'] # type: string + + # prepare types + self.type1 = Types.get(data['Type I'][0]) + self.type2 = None + self.types = [self.type1] # required type + for t in data.get('Type II', []): + self.type2 = Types.get(t) + self.types.append(self.type2) # second type + break + + # base chance to capture pokemon + self.capture_rate = data['CaptureRate'] + # chance of the pokemon to flee away + self.flee_rate = data['FleeRate'] + + # prepare attacks (moves) + self.fast_attacks = self._process_attacks() + self.charged_attack = self._process_attacks(charged=True) + + # prepare movesets + self.movesets = self._process_movesets() + + # Basic Values of the pokemon (identical for all pokemons of one kind) + self.base_attack = data['BaseAttack'] + self.base_defense = data['BaseDefense'] + self.base_stamina = data['BaseStamina'] + + # calculate maximum CP for the pokemon (best IVs, lvl 40) + self.max_cp = _calc_cp(self.base_attack, self.base_defense, + self.base_stamina) + + # + # evolutions info for this pokemon + + # id of the very first level evolution + self.first_evolution_id = self.id + # id of the previous evolution (one level only) + self.prev_evolution_id = None + # ids of all available previous evolutions in the family + self.prev_evolutions_all = [] + if 'Previous evolution(s)' in data: + ids = [int(e['Number']) for e in data['Previous evolution(s)']] + self.first_evolution_id = ids[0] + self.prev_evolution_id = ids[-1] + self.prev_evolutions_all = ids + + # Number of candies for the next evolution (if possible) + self.evolution_cost = 0 + # next evolution flag + self.has_next_evolution = 'Next evolution(s)' in data \ + or 'Next Evolution Requirements' in data + # ids of the last level evolutions + self.last_evolution_ids = [self.id] + # ids of the next possible evolutions (one level only) + self.next_evolution_ids = [] + # ids of all available next evolutions in the family + self.next_evolutions_all = [] + if self.has_next_evolution: + ids = [int(e['Number']) for e in data['Next evolution(s)']] + self.next_evolutions_all = ids + self.evolution_cost = int(data['Next Evolution Requirements']['Amount']) + + @property + def family_id(self): + return self.first_evolution_id + + @property + def is_seen(self): + return pokedex().seen(self.id) + + @property + def is_captured(self): + return pokedex().captured(self.id) + + def _process_movesets(self): + # type: () -> List[Moveset] + """ + The optimal moveset is the combination of two moves, one quick move + and one charge move, that deals the most damage over time. + + Because each quick move gains a certain amount of energy (different + for different moves) and each charge move requires a different amount + of energy to use, sometimes, a quick move with lower DPS will be + better since it charges the charge move faster. On the same note, + sometimes a charge move that has lower DPS will be more optimal since + it may require less energy or it may last for a longer period of time. + + Attacker have STAB (Same-type attack bonus - x1.25) pokemon have the + same type as attack. So we add it to the "Combo DPS" of the moveset. + + The defender attacks in intervals of 1 second for the first 2 attacks, + and then in intervals of 2 seconds for the remainder of the attacks. + This explains why we see two consecutive quick attacks at the beginning + of the match. As a result, we add +2 seconds to the DPS calculation + for defender DPS output. + + So to determine an optimal defensive moveset, we follow the same method + as we did for optimal offensive movesets, but instead calculate the + highest "Combo DPS" with an added 2 seconds to the quick move cool down. + + Note: critical hits have not yet been implemented in the game + + See http://pokemongo.gamepress.gg/optimal-moveset-explanation + See http://pokemongo.gamepress.gg/defensive-tactics + """ + + # Prepare movesets + movesets = [] + for fm in self.fast_attacks: + for chm in self.charged_attack: + movesets.append(Moveset(fm, chm, self.types, self.id)) + assert len(movesets) > 0 + + # Calculate attack perfection for each moveset + movesets = sorted(movesets, key=lambda m: m.dps_attack) + worst_dps = movesets[0].dps_attack + best_dps = movesets[-1].dps_attack + if best_dps > worst_dps: + for moveset in movesets: + current_dps = moveset.dps_attack + moveset.attack_perfection = \ + (current_dps - worst_dps) / (best_dps - worst_dps) + + # Calculate defense perfection for each moveset + movesets = sorted(movesets, key=lambda m: m.dps_defense) + worst_dps = movesets[0].dps_defense + best_dps = movesets[-1].dps_defense + if best_dps > worst_dps: + for moveset in movesets: + current_dps = moveset.dps_defense + moveset.defense_perfection = \ + (current_dps - worst_dps) / (best_dps - worst_dps) + + return sorted(movesets, key=lambda m: m.dps, reverse=True) + + def _process_attacks(self, charged=False): + # type: (bool) -> List[Attack] + key = 'Fast Attack(s)' if not charged else 'Special Attack(s)' + moves_dict = (ChargedAttacks if charged else FastAttacks).BY_NAME + moves = [] + for name in self._data[key]: + if name not in moves_dict: + raise KeyError('Unknown {} attack: "{}"'.format( + 'charged' if charged else 'fast', name)) + moves.append(moves_dict[name]) + moves = sorted(moves, key=lambda m: m.dps, reverse=True) + assert len(moves) > 0 + return moves + + class Pokemon(object): def __init__(self, data): self._data = data @@ -499,6 +670,8 @@ def __init__(self, data): self.id = data.get('id', 0) # Id of the such pokemons in pokedex self.pokemon_id = data['pokemon_id'] + # Static information + self.static = Pokemons.data_for(self.pokemon_id) # Combat points value self.cp = data['cp'] @@ -518,30 +691,27 @@ def __init__(self, data): self.hp = data.get('stamina', self.hp_max) assert 0 <= self.hp <= self.hp_max - # Individial Values of the current pokemon (different for each pokemon) + # Individial Values of the current specific pokemon (different for each) self.iv_attack = data.get('individual_attack', 0) self.iv_defense = data.get('individual_defense', 0) self.iv_stamina = data.get('individual_stamina', 0) - self._static_data = Pokemons.data_for(self.pokemon_id) - self.name = Pokemons.name_for(self.pokemon_id) - self.nickname = data.get('nickname', self.name) + # Basic Values of the pokemon (identical for all pokemons of one kind) + base_attack = self.static.base_attack + base_defense = self.static.base_defense + base_stamina = self.static.base_stamina + + self.name = self.static.name + self.nickname_raw = data.get('nickname', '') + self.nickname = self.nickname_raw or self.name self.in_fort = 'deployed_fort_id' in data self.is_favorite = data.get('favorite', 0) is 1 - # Basic Values of the current pokemon (identical for all such pokemons) - self.base_attack = self._static_data['BaseAttack'] - self.base_defense = self._static_data['BaseDefense'] - self.base_stamina = self._static_data['BaseStamina'] - - # Maximum possible CP for the current pokemon - self.max_cp = self._static_data['max_cp'] - self.fast_attack = FastAttacks.data_for(data['move_1']) self.charged_attack = ChargedAttacks.data_for(data['move_2']) # type: ChargedAttack - # Internal values (IV) perfection percent + # Individial values (IV) perfection percent self.iv = self._compute_iv_perfection() # IV CP perfection - kind of IV perfection percent but calculated @@ -552,12 +722,12 @@ def __init__(self, data): # Exact value of current CP (not rounded) self.cp_exact = _calc_cp( - self.base_attack, self.base_defense, self.base_stamina, + base_attack, base_defense, base_stamina, self.iv_attack, self.iv_defense, self.iv_stamina, self.cp_m) assert max(int(self.cp_exact), 10) == self.cp # Percent of maximum possible CP - self.cp_percent = self.cp_exact / self.max_cp + self.cp_percent = self.cp_exact / self.static.max_cp # Get moveset instance with calculated DPS and perfection percents self.moveset = self._get_moveset() @@ -568,12 +738,16 @@ def __str__(self): def __repr__(self): return self.name + def update_nickname(self, new_nickname): + self.nickname_raw = new_nickname + self.nickname = self.nickname_raw or self.name + def can_evolve_now(self): return self.has_next_evolution() and \ self.candy_quantity >= self.evolution_cost def has_next_evolution(self): - return Pokemons.has_next_evolution(self.pokemon_id) + return self.static.has_next_evolution def has_seen_next_evolution(self): for pokemon_id in self.next_evolution_ids: @@ -583,23 +757,23 @@ def has_seen_next_evolution(self): @property def family_id(self): - return self.first_evolution_id + return self.static.family_id @property def first_evolution_id(self): - return Pokemons.first_evolution_id_for(self.pokemon_id) + return self.static.first_evolution_id @property def prev_evolution_id(self): - return Pokemons.prev_evolution_id_for(self.pokemon_id) + return self.static.prev_evolution_id @property def next_evolution_ids(self): - return Pokemons.next_evolution_ids_for(self.pokemon_id) + return self.static.next_evolution_ids @property def last_evolution_ids(self): - return Pokemons.last_evolution_ids_for(self.pokemon_id) + return self.static.last_evolution_ids @property def candy_quantity(self): @@ -607,7 +781,7 @@ def candy_quantity(self): @property def evolution_cost(self): - return Pokemons.evolution_cost_for(self.pokemon_id) + return self.static.evolution_cost @property def iv_display(self): @@ -640,9 +814,9 @@ def _compute_cp_perfection(self): last_evolution_ids = self.last_evolution_ids for pokemon_id in last_evolution_ids: poke_info = Pokemons.data_for(pokemon_id) - base_attack = poke_info['BaseAttack'] - base_defense = poke_info['BaseDefense'] - base_stamina = poke_info['BaseStamina'] + base_attack = poke_info.base_attack + base_defense = poke_info.base_defense + base_stamina = poke_info.base_stamina # calculate CP variants at maximum level worst_cp = _calc_cp(base_attack, base_defense, base_stamina, @@ -661,7 +835,7 @@ def _compute_cp_perfection(self): def _get_moveset(self): move1 = self.fast_attack move2 = self.charged_attack - movesets = self._static_data['movesets'] + movesets = self.static.movesets current_moveset = None for moveset in movesets: # type: Moveset if moveset.fast_attack == move1 and moveset.charged_attack == move2: @@ -670,12 +844,12 @@ def _get_moveset(self): if current_moveset is None: error = "Unexpected moveset [{}, {}] for #{} {}," \ - " please update info in pokemon.json and create issue/PR"\ + " please update info in pokemon.json and create issue/PR" \ .format(move1, move2, self.pokemon_id, self.name) # raise ValueError(error) logging.getLogger(type(self).__name__).error(error) current_moveset = Moveset( - move1, move2, self._static_data['types'], self.pokemon_id) + move1, move2, self.static.types, self.pokemon_id) return current_moveset @@ -685,7 +859,7 @@ def __init__(self, data): # self._data = data # Not needed - all saved in fields self.id = data['id'] self.name = data['name'] - self.type = data['type'] + self.type = Types.get(data['type']) self.damage = data['damage'] self.duration = data['duration'] / 1000.0 # duration in seconds @@ -710,6 +884,14 @@ def dps_with_stab(self): # DPS with STAB (Same-type attack bonus) return self.dps * STAB_FACTOR + @property + def effective_against(self): + return self.type.attack_effective_against + + @property + def weak_against(self): + return self.type.attack_weak_against + @property def energy_per_second(self): return self.energy / self.duration @@ -741,9 +923,9 @@ def is_charged(self): class Moveset(object): def __init__(self, fm, chm, pokemon_types=(), pokemon_id=-1): - # type: (Attack, ChargedAttack, List[string], int) -> None + # type: (Attack, ChargedAttack, List[Type], int) -> None if len(pokemon_types) <= 0 < pokemon_id: - pokemon_types = Pokemons.data_for(pokemon_id)['types'] + pokemon_types = Pokemons.data_for(pokemon_id).types self.pokemon_id = pokemon_id self.fast_attack = fm @@ -785,8 +967,8 @@ def __init__(self, fm, chm, pokemon_types=(), pokemon_id=-1): # DPS for attack (counting STAB) self.dps_attack = (fm_damage + chm_damage) / (fm_secs + chm_secs) - # Moveset perfection percent attack and for defense - # Calculated for current pokemon, not between all pokemons + # Moveset perfection percent for attack and for defense + # Calculated for current pokemon kind only, not between all pokemons # So 100% perfect moveset can be weak if pokemon is weak (e.g. Caterpie) self.attack_perfection = .0 self.defense_perfection = .0 @@ -834,21 +1016,23 @@ def retrieve_item_inventory_size(self): # -# Usage helpers +# Other # STAB (Same-type attack bonus) +# Factor applied to attack of the same type as pokemon STAB_FACTOR = 1.25 +# Factor applied to attack when it's effective against defending pokemon type +EFFECTIVENESS_FACTOR = 1.25 +# Factor applied to attack when it's weak against defending pokemon type +RESISTANCE_FACTOR = 0.8 -_inventory = None -LevelToCPm() # init LevelToCPm -FastAttacks() # init FastAttacks -ChargedAttacks() # init ChargedAttacks +_inventory = None # type: Inventory def _calc_cp(base_attack, base_defense, base_stamina, iv_attack=15, iv_defense=15, iv_stamina=15, - cp_multiplier=LevelToCPm.MAX_CPM): + cp_multiplier=.0): """ CP calculation @@ -870,12 +1054,31 @@ def _calc_cp(base_attack, base_defense, base_stamina, :param cp_multiplier: CP Multiplier (0.79030001 is max - value for level 40) :return: CP as float """ + assert base_attack > 0 + assert base_defense > 0 + assert base_stamina > 0 + + if cp_multiplier <= .0: + cp_multiplier = LevelToCPm.MAX_CPM + assert cp_multiplier > .0 + return (base_attack + iv_attack) \ * ((base_defense + iv_defense)**0.5) \ * ((base_stamina + iv_stamina)**0.5) \ * (cp_multiplier ** 2) / 10 +# Initialize static data in the right order +Types() # init Types +LevelToCPm() # init LevelToCPm +FastAttacks() # init FastAttacks +ChargedAttacks() # init ChargedAttacks +Pokemons() # init Pokemons + + +# +# Usage helpers + def init_inventory(bot): global _inventory _inventory = Inventory(bot) @@ -884,10 +1087,12 @@ 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 @@ -908,6 +1113,10 @@ def items(): return _inventory.items +def types_data(): + return Types + + def levels_to_cpm(): return LevelToCPm diff --git a/tests/inventory_test.py b/tests/inventory_test.py index 3d5ffd66b6..4070344ca4 100644 --- a/tests/inventory_test.py +++ b/tests/inventory_test.py @@ -4,6 +4,24 @@ class InventoryTest(unittest.TestCase): + def test_types(self): + td = Types + self.assertIs(types_data(), td) + self.assertEqual(len(td.STATIC_DATA), 18) + self.assertEqual(len(td.all()), 18) + + for name, s in td.STATIC_DATA.iteritems(): + assert len(name) > 0 + self.assertIs(s.name, name) + for t in s.attack_effective_against: + self.assertIn(s, t.pokemon_vulnerable_to) + for t in s.attack_weak_against: + self.assertIn(s, t.pokemon_resistant_to) + for t in s.pokemon_vulnerable_to: + self.assertIn(s, t.attack_effective_against) + for t in s.pokemon_resistant_to: + self.assertIn(s, t.attack_weak_against) + def test_pokemons(self): # Init data self.assertEqual(len(Pokemons().all()), 0) # No inventory loaded here @@ -11,53 +29,64 @@ def test_pokemons(self): obj = Pokemons self.assertEqual(len(obj.STATIC_DATA), 151) - for poke_info in obj.STATIC_DATA: - name = poke_info['Name'] - pokemon_id = int(poke_info['Number']) - self.assertTrue(1 <= pokemon_id <= 151) - - self.assertGreaterEqual(len(poke_info['movesets']), 1) - self.assertTrue(262 <= poke_info['max_cp'] <= 4145) - self.assertTrue(1 <= len(poke_info['types']) <= 2) - self.assertTrue(40 <= poke_info['BaseAttack'] <= 284) - self.assertTrue(54 <= poke_info['BaseDefense'] <= 242) - self.assertTrue(20 <= poke_info['BaseStamina'] <= 500) - self.assertTrue(.0 <= poke_info['CaptureRate'] <= .56) - self.assertTrue(.0 <= poke_info['FleeRate'] <= .99) - self.assertTrue(1 <= len(poke_info['Weaknesses']) <= 7) - self.assertTrue(3 <= len(name) <= 10) - - self.assertGreaterEqual(len(poke_info['Classification']), 11) - self.assertGreaterEqual(len(poke_info['Fast Attack(s)']), 1) - self.assertGreaterEqual(len(poke_info['Special Attack(s)']), 1) - - self.assertIs(obj.data_for(pokemon_id), poke_info) + for idx in xrange(len(obj.STATIC_DATA)): + pokemon = obj.STATIC_DATA[idx] # type: PokemonInfo + name = pokemon.name + pokemon_id = pokemon.id + self.assertEqual(pokemon.id, idx+1) + assert (1 <= pokemon_id <= 151) + + self.assertGreaterEqual(len(pokemon.movesets), 1) + self.assertIsInstance(pokemon.movesets[0], Moveset) + assert 262 <= pokemon.max_cp <= 4145 + assert 1 <= len(pokemon.types) <= 2 + assert 40 <= pokemon.base_attack <= 284 + assert 54 <= pokemon.base_defense <= 242 + assert 20 <= pokemon.base_stamina <= 500 + assert .0 <= pokemon.capture_rate <= .56 + assert .0 <= pokemon.flee_rate <= .99 + assert 1 <= len(pokemon._data['Weaknesses']) <= 7 + assert 3 <= len(name) <= 10 + + self.assertGreaterEqual(len(pokemon.classification), 11) + self.assertGreaterEqual(len(pokemon.fast_attacks), 1) + self.assertGreaterEqual(len(pokemon.charged_attack), 1) + + self.assertIs(obj.data_for(pokemon_id), pokemon) self.assertIs(obj.name_for(pokemon_id), name) first_evolution_id = obj.first_evolution_id_for(pokemon_id) + self.assertIs(first_evolution_id, pokemon.first_evolution_id) + self.assertIs(pokemon.family_id, first_evolution_id) self.assertGreaterEqual(first_evolution_id, 1) next_evolution_ids = obj.next_evolution_ids_for(pokemon_id) + self.assertIs(next_evolution_ids, pokemon.next_evolution_ids) last_evolution_ids = obj.last_evolution_ids_for(pokemon_id) + self.assertIs(last_evolution_ids, pokemon.last_evolution_ids) candies_cost = obj.evolution_cost_for(pokemon_id) - obj.prev_evolution_id_for(pokemon_id) # just call test + self.assertIs(candies_cost, pokemon.evolution_cost) + self.assertIs(obj.prev_evolution_id_for(pokemon_id), pokemon.prev_evolution_id) self.assertGreaterEqual(len(last_evolution_ids), 1) if not obj.has_next_evolution(pokemon_id): - assert 'Next evolution(s)' not in poke_info - assert 'Next Evolution Requirements' not in poke_info + assert not pokemon.has_next_evolution + self.assertEqual(pokemon.evolution_cost, 0) + self.assertEqual(pokemon.next_evolution_ids, []) + self.assertEqual(pokemon.next_evolutions_all, []) + self.assertEqual(pokemon.last_evolution_ids, [pokemon_id]) else: + self.assertGreater(candies_cost, 0) self.assertGreaterEqual(len(next_evolution_ids), 1) self.assertLessEqual(len(next_evolution_ids), len(last_evolution_ids)) - reqs = poke_info['Next Evolution Requirements'] + reqs = pokemon._data['Next Evolution Requirements'] self.assertEqual(reqs["Family"], first_evolution_id) candies_name = obj.name_for(first_evolution_id) + ' candies' self.assertEqual(reqs["Name"], candies_name) - self.assertIsNotNone(candies_cost) - self.assertTrue(12 <= candies_cost <= 400) + assert 12 <= candies_cost <= 400 self.assertEqual(reqs["Amount"], candies_cost) - evolutions = poke_info["Next evolution(s)"] + evolutions = pokemon._data["Next evolution(s)"] self.assertGreaterEqual(len(evolutions), len(next_evolution_ids)) for p in evolutions: @@ -67,7 +96,7 @@ def test_pokemons(self): for p_id in next_evolution_ids: self.assertEqual(obj.prev_evolution_id_for(p_id), pokemon_id) - prev_evs = obj.data_for(p_id)["Previous evolution(s)"] + prev_evs = obj.data_for(p_id)._data["Previous evolution(s)"] self.assertGreaterEqual(len(prev_evs), 1) self.assertEqual(int(prev_evs[-1]["Number"]), pokemon_id) self.assertEqual(prev_evs[-1]["Name"], name) @@ -76,8 +105,8 @@ def test_pokemons(self): self.assertEqual(len(next_evolution_ids), 1 if pokemon_id != 133 else 3) - if "Previous evolution(s)" in poke_info: - for p in poke_info["Previous evolution(s)"]: + if "Previous evolution(s)" in pokemon._data: + for p in pokemon._data["Previous evolution(s)"]: p_id = int(p["Number"]) self.assertNotEqual(p_id, pokemon_id) self.assertEqual(p["Name"], obj.name_for(p_id)) @@ -96,11 +125,12 @@ def test_pokemons(self): self.assertEqual(poke.level, 12.5) self.assertEqual(poke.iv, 0.47) self.assertAlmostEqual(poke.ivcp, 0.488747515) - self.assertAlmostEqual(poke.max_cp, 1921.34561459) + self.assertAlmostEqual(poke.static.max_cp, 1921.34561459) self.assertAlmostEqual(poke.cp_percent, 0.340368964) - self.assertTrue(poke.is_favorite) + assert poke.is_favorite self.assertEqual(poke.name, 'Golbat') self.assertEqual(poke.nickname, "Golb") + self.assertEqual(poke.nickname_raw, poke.nickname) self.assertAlmostEqual(poke.moveset.dps, 10.7540173053) self.assertAlmostEqual(poke.moveset.dps_attack, 12.14462299) self.assertAlmostEqual(poke.moveset.dps_defense, 4.876681614) @@ -114,11 +144,12 @@ def test_pokemons(self): self.assertEqual(poke.level, 7.5) self.assertEqual(poke.iv, 0.44) self.assertAlmostEqual(poke.ivcp, 0.3804059) - self.assertAlmostEqual(poke.max_cp, 581.64643575) + self.assertAlmostEqual(poke.static.max_cp, 581.64643575) self.assertAlmostEqual(poke.cp_percent, 0.183759867) self.assertFalse(poke.is_favorite) self.assertEqual(poke.name, 'Rattata') - self.assertEqual(poke.nickname, 'Rattata') + self.assertEqual(poke.nickname, poke.name) + self.assertEqual(poke.nickname_raw, '') self.assertAlmostEqual(poke.moveset.dps, 12.5567813108) self.assertAlmostEqual(poke.moveset.dps_attack, 15.6959766385) self.assertAlmostEqual(poke.moveset.dps_defense, 5.54282440561) @@ -156,7 +187,7 @@ def _test_attacks(self, callback, clazz): # check consistency attacks = clazz.all_by_dps() number = len(attacks) - self.assertTrue(number > 0) + assert (number > 0) self.assertGreaterEqual(len(clazz.BY_TYPE), 17) self.assertEqual(number, len(clazz.all())) self.assertEqual(number, len(clazz.STATIC_DATA)) @@ -168,16 +199,17 @@ def _test_attacks(self, callback, clazz): for attack in attacks: # type: Attack self.assertGreater(attack.id, 0) self.assertGreater(len(attack.name), 0) - self.assertGreater(len(attack.type), 0) + self.assertIsInstance(attack.type, Type) self.assertGreaterEqual(attack.damage, 0) self.assertGreater(attack.duration, .0) self.assertGreater(attack.energy, 0) self.assertGreaterEqual(attack.dps, 0) - self.assertTrue(.0 <= attack.rate_in_type <= 1.0) + assert (.0 <= attack.rate_in_type <= 1.0) self.assertLessEqual(attack.dps, prev_dps) self.assertEqual(attack.is_charged, charged) self.assertIs(attack, clazz.data_for(attack.id)) self.assertIs(attack, clazz.by_name(attack.name)) - self.assertTrue(attack in clazz.BY_TYPE[attack.type]) + assert (attack in clazz.list_for_type(attack.type)) + assert (attack in clazz.list_for_type(attack.type.name)) self.assertIsInstance(attack, ChargedAttack if charged else Attack) prev_dps = attack.dps diff --git a/tests/nickname_test.py b/tests/nickname_test.py new file mode 100644 index 0000000000..6d35d55c55 --- /dev/null +++ b/tests/nickname_test.py @@ -0,0 +1,91 @@ +import unittest + +from pokemongo_bot.cell_workers import NicknamePokemon +from pokemongo_bot.inventory import Pokemon + + +class NicknamePokemonTest(unittest.TestCase): + def test_nickname_generation(self): + # basic + self.assertNicks('', ['', '']) + self.assertNicks('{pokemon}', ['', '']) + self.assertNicks('{name}', ['', '']) + self.assertNicks('{Name}', ['', '']) + self.assertNicks('{id}', ['42', '19']) + self.assertNicks('{cp}', ['653', '106']) + self.assertNicks('{CP}', ['653', '106']) + self.assertNicks('{iv_attack}', ['9', '6']) + self.assertNicks('{iv_defense}', ['4', '14']) + self.assertNicks('{iv_stamina}', ['8', '0']) + self.assertNicks('{iv_ads}', ['9/4/8', '6/14/0']) + self.assertNicks('{iv_sum}', ['21', '20']) + self.assertNicks('{iv_pct}', ['047', '044']) + self.assertNicks('{iv_pct2}', ['46', '44']) + self.assertNicks('{iv_pct1}', ['4', '4']) + self.assertNicks('{base_attack}', ['164', '92']) + self.assertNicks('{base_defense}', ['164', '86']) + self.assertNicks('{base_stamina}', ['150', '60']) + self.assertNicks('{base_ads}', ['164/164/150', '92/86/60']) + self.assertNicks('{attack}', ['173', '98']) + self.assertNicks('{defense}', ['168', '100']) + self.assertNicks('{stamina}', ['158', '60']) + self.assertNicks('{sum_ads}', ['173/168/158', '98/100/60']) + self.assertNicks('{ivcp_pct}', ['049', '038']) + self.assertNicks('{ivcp_pct2}', ['48', '38']) + self.assertNicks('{ivcp_pct1}', ['4', '3']) + self.assertNicks('{fast_attack_char}', ['L', 'n']) + self.assertNicks('{charged_attack_char}', ['h', 'n']) + self.assertNicks('{attack_code}', ['Lh', 'nn']) + self.assertNicks('{attack_pct}', ['047', '084']) + self.assertNicks('{attack_pct2}', ['47', '83']) + self.assertNicks('{attack_pct1}', ['4', '8']) + self.assertNicks('{defense_pct}', ['082', '060']) + self.assertNicks('{defense_pct2}', ['81', '60']) + self.assertNicks('{defense_pct1}', ['7', '5']) + + # complex + self.assertNicks('{name:2}', ['', '']) + self.assertNicks('{pokemon.iv:.2%}', ['47.00%', '44.00%']) + self.assertNicks('{pokemon.fast_attack}', ['Wing Attack', 'Tackle']) + self.assertNicks('{pokemon.charged_attack}', ['Ominous Wind', 'Hyper Fang']) + self.assertNicks('{pokemon.fast_attack.type}', ['Flying', 'Normal']) + self.assertNicks('{pokemon.fast_attack.dps:.2f}', ['12.00', '10.91']) + self.assertNicks('{pokemon.fast_attack.dps:.0f}', ['12', '11']) + self.assertNicks('{iv_pct}_{iv_ads}', ['047_9/4/8', '044_6/14/0']) + self.assertNicks( + '{ivcp_pct2}_{iv_pct2}_{iv_ads}', + ['48_46_9/4/8', '38_44_6/14/0']) + self.assertNicks( + '{attack_code}{attack_pct1}{defense_pct1}{ivcp_pct1}{name}', + ['Lh474Golbat', 'nn853Rattata']) + + # + def setUp(self): + self.bot = {} + self.config = {} + self.task = NicknamePokemon(self.bot, self.config) + self.assertIs(self.task.bot, self.bot) + self.assertIs(self.task.config, self.config) + + self.pokemons = [ + Pokemon({ + "num_upgrades": 2, "move_1": 210, "move_2": 69, "pokeball": 2, + "favorite": 1, "pokemon_id": 42, "battles_attacked": 4, + "stamina": 76, "stamina_max": 76, "individual_attack": 9, + "individual_defense": 4, "individual_stamina": 8, + "cp_multiplier": 0.4627983868122101, + "additional_cp_multiplier": 0.018886566162109375, + "cp": 653, "nickname": "Golb", "id": 13632861873471324}), + Pokemon({ + "move_1": 221, "move_2": 129, "pokemon_id": 19, "cp": 106, + "individual_attack": 6, "stamina_max": 22, "individual_defense": 14, + "cp_multiplier": 0.37523558735847473, "id": 7841053399}), + ] + + def assertNicks(self, template, expected_results): + real_results = [self.task._generate_new_nickname(p, template) + for p in self.pokemons] + self.assertListEqual(list(expected_results), real_results) + + # helper for test generation + # print "self.assertNicks('{}', {})".format(template, real_results) From 471515d50a1eb346185dd9710df6409a77eaf925 Mon Sep 17 00:00:00 2001 From: Abel Ingrand Date: Fri, 12 Aug 2016 19:56:59 +0200 Subject: [PATCH 189/202] Added Procfile to deploy to Heroku (#3719) --- Procfile | 1 + 1 file changed, 1 insertion(+) create mode 100644 Procfile diff --git a/Procfile b/Procfile new file mode 100644 index 0000000000..e8beec9ae2 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +worker: python pokecli.py $EXTRA_ARGS From 912970af70393a308f6f2c13548a8bc81bc9befa Mon Sep 17 00:00:00 2001 From: devn0ll Date: Fri, 12 Aug 2016 20:00:03 +0200 Subject: [PATCH 190/202] Update installation.md (#3764) editet Manual install Mac section --- docs/installation.md | 60 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index b8951f62d2..3f68512d1b 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -116,12 +116,60 @@ source bin/activate ### Installation Mac (change master to dev for the latest version) -``` -$ git clone --recursive -b master https://github.com/PokemonGoF/PokemonGo-Bot -$ cd PokemonGo-Bot -$ virtualenv . -$ source bin/activate -$ pip install -r requirements.txt +```bash +##install +#go to your home directory with the console +brew install --devel protobuf +brew install autoconf libtool pkg-config wget git +#install pip +wget https://bootstrap.pypa.io/get-pip.py +python2.7 get-pip.py +rm -f get-pip.py +#get git repo +git clone --recursive -b master https://github.com/PokemonGoF/PokemonGo-Bot +cd PokemonGo-Bot +#install and enable virtualenv +#You need to make shure your python version and virtualenv verison work together +#install virtualenv and activate it +pip install virtualenv +virtualenv . +source bin/activate +#then install the requierements +pip install -r requirements.txt + +##get the encryption.so and move to right folder +wget http://pgoapi.com/pgoencrypt.tar.gz +tar -xzvf pgoencrypt.tar.gz +cd pgoencrypt/src/ +make +cd ../../ +#make the encrypt able to load +mv pgoencrypt/src/libencrypt.so encrypt.so + +##edit the configuration file +cp configs/config.json.example configs/config.json +vi configs/config.json +# gedit is possible too with 'gedit configs/config.json' +#edit "google" to "ptc" if you have a pokemon trainer account +#edit all settings + + +##update to newest +#if you need to do more i'll update this file +#make shure virtualenv is enabled and you are in the correct folder +git pull +pip install -r requirements.txt + +##start the bot +./run.sh configs/config.json + +##after reboot or closing the terminal +#at every new start go into the folder of the PokemonGo-Bot by +#going into the folder where you startet installing it an then +cd PokemonGo-Bot +#activate virtualenv and start +source bin/activate +./run.sh configs/config.json ``` ### Installation Windows From 5a8a95ad228aa3a00ed7e490cbcc9afa3f8c1a80 Mon Sep 17 00:00:00 2001 From: Eli White Date: Fri, 12 Aug 2016 11:10:36 -0700 Subject: [PATCH 191/202] Writing the location file to fix the web ui (#3767) --- pokemongo_bot/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 88fd9916b4..eabac4e9f3 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -1017,10 +1017,9 @@ def heartbeat(self): pass def update_web_location_worker(self): - pass - # while True: - # self.web_update_queue.get() - # self.update_web_location() + while True: + self.web_update_queue.get() + self.update_web_location() def get_inventory_count(self, what): response_dict = self.get_inventory() From cd084b7abaef129a147ff57f28cbe68d2d01c4bc Mon Sep 17 00:00:00 2001 From: Dmitry Ovodov Date: Sat, 13 Aug 2016 01:26:12 +0700 Subject: [PATCH 192/202] Revert #3500 Fix error when MoveToFort called from handle_soft_ban.py (#3772) Useless since #3629 was merged --- pokemongo_bot/cell_workers/move_to_fort.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 24ecf5e74a..2be287e86c 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -13,13 +13,9 @@ class MoveToFort(BaseTask): def initialize(self): self.lure_distance = 0 - if self.config: - 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) - else: - self.lure_attraction = None - self.ignore_item_count = True + 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() From a3d1f361d23594fb72b5b188698151e297cc95f5 Mon Sep 17 00:00:00 2001 From: joaodragao Date: Fri, 12 Aug 2016 20:12:53 +0100 Subject: [PATCH 193/202] Clean old catch parameters (#3776) * `catch_randomize_reticle_factor` * `catch_randomize_spin_factor` --- pokecli.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/pokecli.py b/pokecli.py index ef50212d9f..064c87c9ba 100644 --- a/pokecli.py +++ b/pokecli.py @@ -404,22 +404,6 @@ def _json_loader(filename): type=bool, default=True, ) - 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)", - type=float, - default=1.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)", - type=float, - default=1.0 - ) add_config( parser, load, @@ -539,14 +523,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 - plugin_loader = PluginLoader() for plugin in config.plugins: plugin_loader.load_plugin(plugin) From c450b19a3e44cc660576940009da6e7c85a0121f Mon Sep 17 00:00:00 2001 From: pmquan Date: Fri, 12 Aug 2016 12:28:33 -0700 Subject: [PATCH 194/202] Fix incorrect variable name in pokemon_catch_worker that makes bot unusable (#3780) --- CONTRIBUTORS.md | 2 +- pokemongo_bot/cell_workers/pokemon_catch_worker.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 1e463bb8d3..c363f3d804 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -68,4 +68,4 @@ * joaodragao * extink * Quantra - + * pmquan diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 4030a33e41..d30718eaea 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -367,7 +367,7 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): 'encounter_id': self.pokemon['encounter_id'], 'latitude': self.pokemon['latitude'], 'longitude': self.pokemon['longitude'], - 'pokemon_id': pokemon.num + 'pokemon_id': pokemon.pokemon_id } ) if self._pct(catch_rate_by_ball[current_ball]) == 100: From cf8d2bf58627453fee30f7630e113524b7ebef33 Mon Sep 17 00:00:00 2001 From: Quantra Date: Sat, 13 Aug 2016 02:24:54 +0100 Subject: [PATCH 195/202] added action_delay when recycling items (#3799) --- pokemongo_bot/cell_workers/recycle_items.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 4b6ea8c3ca..9fa2518e35 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -4,6 +4,7 @@ from pokemongo_bot import inventory from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.human_behaviour import action_delay from pokemongo_bot.services.item_recycle_worker import ItemRecycler from pokemongo_bot.tree_config_builder import ConfigException from pokemongo_bot.worker_result import WorkerResult @@ -87,6 +88,7 @@ def work(self): amount_to_recycle = self.get_amount_to_recycle(item_in_inventory) if self.item_should_be_recycled(item_in_inventory, amount_to_recycle): + action_delay(self.bot.config.action_wait_min, self.bot.config.action_wait_max) if ItemRecycler(self.bot, item_in_inventory, amount_to_recycle).work() == WorkerResult.ERROR: worker_result = WorkerResult.ERROR From 9b40f58f215bca574cb79e9956500fa2f83f39d9 Mon Sep 17 00:00:00 2001 From: Anakin5 Date: Sat, 13 Aug 2016 09:28:20 +0800 Subject: [PATCH 196/202] Pokemon optimizer enhancements (#3743) * catching every single pokemon nearby * catch lured pokemon in all forts nearby * adding run_interval to some tasks to avoid running all the time and minimum tick time of 5 seconds Tasks inheriting from BaseTask should use `self._update_last_ran` and `_time_to_run` if they want to implement the time based running. The config to set a custom timer is named `run_interval`. * added config to ignore item count for Spin and MoveToFort this works good with the `run_interval` configuration added to TransferPokemon and RecycleItem * spinning all pokestops in range * fixing loop in spin fort task * First basic features of the pokemon optimizer * For now, dry run only * Add cygwin to supported platform and improved log readability (#2948) * Add cygwin to supported platform and improved log readability * fixed formatting * - Add dry_run and use_lucky_egg in config - Evolve all pokemons together and only if enough for a full lucky egg (90). - Keep enough candies for consecutive evolutions of best pokemons - Only evolve the lowest rank of a family * Add lucky egg support when enough pokemon to evolve * fixing returns * - Support Eevee evolution scheme - Rename "use_lucky_egg" parameter in the more accurate "evolve_only_with_lucky_egg" * Revert "Merge remote-tracking branch 'origin/faeture/xp-improvements' into pokemon_optimizer" This reverts commit ff1f5e4bd3ec66b904625ec26b969f57ae6aaeb8, reversing changes made to e8fd90137e53409e87f8fdcf341916cf6d551481. * - Fix an issue in evolve_pokemon task - Use common inventory - Add configuration example * Add missing inventory refresh at the end of the process * Add missing inventory refresh after catching a pokemon * Add parameters "transfer" and "evolve" to activate/deactivate corresponding action. If both false, this is equivalent to a dry_run. Add parameters "use_lucky_egg" to allow task to use a lucky egg before evolve. Add parameter "minimum_evolve_for_lucky_egg" to add a requirement on the number of evolution before using a lucky egg. * Move some functions around * Default lucky egg to false + had again parameter "evolve_only_with_lucky_egg" * Fix qn issue with egg counting Add configuration parameter to allow customization of how pokemons are ranked in a family * Update configuration example * Upgrade to latest inventory * Fix bug * Add parameter "use_candies_for_xp" to activate/deactivate usage of candies to maximize xp Add comments in the configuration example * Add dps, dps_attack and dps_defense in available sorting keys. So you can now keep the best move. Add more comments in config Display ncp and dps for released and evolved pokemons * Update inventory when releasing and evolving pokemons * Display Pokemon Bag count update --- configs/config.json.optimizer.example | 56 ++++++- pokemongo_bot/__init__.py | 4 +- pokemongo_bot/cell_workers/evolve_pokemon.py | 4 +- .../cell_workers/pokemon_optimizer.py | 139 ++++++++++++------ .../cell_workers/transfer_pokemon.py | 4 +- 5 files changed, 152 insertions(+), 55 deletions(-) diff --git a/configs/config.json.optimizer.example b/configs/config.json.optimizer.example index 4db7f2cee6..2e91f525d8 100644 --- a/configs/config.json.optimizer.example +++ b/configs/config.json.optimizer.example @@ -21,25 +21,74 @@ { "type": "PokemonOptimizer", "config": { + "// the 'transfer' parameter activate or deactivate the transfer of pokemons": {}, + "// at false, no pokemon is going to be transfered, ever": {}, + "// at false, you will still get the log information of what the optimizer": {}, + "// would have transfered if the parameter was true": {}, "transfer": true, + "// the 'evolve' parameter activate or deactivate the evolution of pokemons": {}, + "// at false, no pokemon is going to be evolved, ever": {}, + "// at false, you will still get the log information of what the": {}, + "// optimizer would have evolved if the parameter was true": {}, "evolve": true, + "// the 'use_candies_for_xp' parameter let you choose if you want the optimizer": {}, + "// to use your candies to evolve low quality pokemons in order to maximize your xp": {}, + "// at false, the optimizer will still use candies to evolve your best Pokemons": {}, + "use_candies_for_xp": true, + "// the 'use_lucky_egg' parameter let you choose if you want the optimizer": {}, + "// to use a lucky egg right before evolving Pokemons. At false; the optimizer": {}, + "// is free to evolve Pokemons even if you do not have any lucky egg.": {}, "use_lucky_egg": true, + "// the 'evolve_only_with_lucky_egg' parameter let you choose if you want the optimizer": {}, + "// to only Evolve Pokemons when a lucky egg is available": {}, "evolve_only_with_lucky_egg": true, + "// the 'minimum_evolve_for_lucky_egg' parameter let you define the minimum": {}, + "// number of Pokemons that must evolve before using a lucky egg": {}, + "// If that number is not reached, and evolve_only_with_lucky_egg is true, evolution will be skipped": {}, + "// If that number is not reached, and evolve_only_with_lucky_egg is false,": {}, + "// evolution will be performed without using a lucky egg": {}, "minimum_evolve_for_lucky_egg": 90, + "// the 'keep' parameter let you define what pokemons you consider are the 'best'. These Pokemons": {}, + "// will be keep and evolved. Note that Pokemons are evaluated inside their whole family": {}, + "// Multiple way of ranking can be defined. Following configuration let you keep the best iv,": {}, + "// the best ncp and the best cp": {}, "keep": [ { + "// Following setting let you keep the best iv of the family": {}, + "// the 'top' parameter allow you to define how many Pokemons you want to keep": {}, + "// at the top of your ranking. If several Pokemons get the same score, they are": {}, + "// considered equal. Thus, top=1 might result in keeping more than 1 Pokemon.": {}, "top": 1, + "// the 'evolve' parameter let you choose if you want to evolve the Pokemons you keep": {}, "evolve": true, - "// Available sorting keys are:": true, - "// iv, cp, ncp, ivcp, max_cp, iv_attack, iv_defense, iv_stamina, hp_max, level": true, + "// the 'sort' parameter define how you want to rank your pokemons": {}, + "// Critera are sorted fro, the most important to the least important.": {}, + "// Available criteria are:": {}, + "// 'iv' = individual value": {}, + "// 'ivcp' = iv weigted so that for equal iv, attack > defense > stamina": {}, + "// 'cp' = combat power (can be increased with candies)": {}, + "// 'cp_exact' = combar power (not rounded)": {}, + "// 'ncp' (normalized cp) or 'cp_percent' = ratio cp / max_cp": {}, + "// iv_attack = attach component of iv": {}, + "// iv_defense = defense component of iv": {}, + "// iv_stamina = stamina component of iv": {}, + "// dps = raw dps based on the moves of the pokemon": {}, + "// dps_attack = average dps when attacking": {}, + "// dps_defense = average dps when defending": {}, + "// Note that the more criteria you add to this list, the less likely Pokemons": {}, + "// will be equals": {}, "sort": ["iv"] }, { + "// Following setting let you keep keep the best normalized cp of the family": {}, + "// That is the Pokemon with higher CP once fully evolved": {}, "top": 1, "evolve": true, "sort": ["ncp"] }, { + "// Following setting let you keep keep the best cp of the family.": {}, + "// But will not evolve it further (in favor of the best ncp)": {}, "top": 1, "evolve": false, "sort": ["cp"] @@ -76,7 +125,7 @@ { "type": "MoveToFort", "config": { - "lure_attraction": false, + "lure_attraction": true, "lure_max_distance": 2000, "ignore_item_count": true } @@ -97,6 +146,7 @@ "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, + "min_ultraball_to_keep": 10, "logging_color": true, "catch": { "any": { diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index eabac4e9f3..ff5257c043 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -300,7 +300,7 @@ def _register_events(self): ) self.event_manager.register_event( 'pokemon_evolved', - parameters=('pokemon', 'iv', 'cp', 'xp') + parameters=('pokemon', 'iv', 'cp', 'ncp', 'dps', 'xp') ) self.event_manager.register_event('skip_evolve') self.event_manager.register_event('threw_berry_failed', parameters=('status_code',)) @@ -392,7 +392,7 @@ def _register_events(self): ) self.event_manager.register_event( 'pokemon_release', - parameters=('pokemon', 'cp', 'iv') + parameters=('pokemon', 'iv', 'cp', 'ncp', 'dps') ) # polyline walker diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index b5675aba8f..4de40fdb7d 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -108,7 +108,9 @@ def _execute_pokemon_evolve(self, pokemon, cache): 'pokemon': pokemon.name, 'iv': pokemon.iv, 'cp': pokemon.cp, - 'xp': 0 + 'ncp': '?', + 'dps': '?', + 'xp': '?' } ) awarded_candies = response_dict.get('responses', {}).get('EVOLVE_POKEMON', {}).get('candy_awarded', 0) diff --git a/pokemongo_bot/cell_workers/pokemon_optimizer.py b/pokemongo_bot/cell_workers/pokemon_optimizer.py index ba7072eb2d..81f78ddf33 100644 --- a/pokemongo_bot/cell_workers/pokemon_optimizer.py +++ b/pokemongo_bot/cell_workers/pokemon_optimizer.py @@ -13,10 +13,12 @@ class PokemonOptimizer(BaseTask): def initialize(self): self.family_by_family_id = {} + self.last_pokemon_count = 0 self.logger = logging.getLogger(self.__class__.__name__) self.config_transfer = self.config.get("transfer", False) self.config_evolve = self.config.get("evolve", False) + self.config_use_candies_for_xp = self.config.get("use_candies_for_xp", True) self.config_use_lucky_egg = self.config.get("use_lucky_egg", False) self.config_evolve_only_with_lucky_egg = self.config.get("evolve_only_with_lucky_egg", True) self.config_minimum_evolve_for_lucky_egg = self.config.get("minimum_evolve_for_lucky_egg", 90) @@ -25,7 +27,13 @@ def initialize(self): {"top": 1, "evolve": False, "sort": ["cp"]}]) def get_pokemon_slot_left(self): - return self.bot._player["max_pokemon_storage"] - len(inventory.pokemons()._data) + pokemon_count = len(inventory.pokemons()._data) + + if pokemon_count != self.last_pokemon_count: + self.last_pokemon_count = pokemon_count + self.logger.info("Pokemon Bag: %s/%s", pokemon_count, self.bot._player["max_pokemon_storage"]) + + return self.bot._player["max_pokemon_storage"] - pokemon_count def work(self): if self.get_pokemon_slot_left() > 5: @@ -56,6 +64,9 @@ def parse_inventory(self): for pokemon in inventory.pokemons().all(): family_id = pokemon.first_evolution_id setattr(pokemon, "ncp", pokemon.cp_percent) + setattr(pokemon, "dps", pokemon.moveset.dps) + setattr(pokemon, "dps_attack", pokemon.moveset.dps_attack) + setattr(pokemon, "dps_defense", pokemon.moveset.dps_defense) self.family_by_family_id.setdefault(family_id, []).append(pokemon) @@ -173,23 +184,27 @@ def get_evolution_plan(self, family_id, family, evolve_best, keep_best): next_evo.name = inventory.pokemons().name_for(next_pid) evolve_best.append(next_evo) - # Compute how many crap we should keep if we want to batch evolve them for xp - junior_evolution_cost = inventory.pokemons().evolution_cost_for(family_id) + if self.config_use_candies_for_xp: + # Compute how many crap we should keep if we want to batch evolve them for xp + junior_evolution_cost = inventory.pokemons().evolution_cost_for(family_id) - # transfer + keep_for_evo = len(crap) - # leftover_candies = candies - len(crap) + transfer * 1 - # keep_for_evo = leftover_candies / junior_evolution_cost - # - # keep_for_evo = (candies - len(crap) + transfer) / junior_evolution_cost - # keep_for_evo = (candies - keep_for_evo) / junior_evolution_cost + # transfer + keep_for_evo = len(crap) + # leftover_candies = candies - len(crap) + transfer * 1 + # keep_for_evo = leftover_candies / junior_evolution_cost + # + # keep_for_evo = (candies - len(crap) + transfer) / junior_evolution_cost + # keep_for_evo = (candies - keep_for_evo) / junior_evolution_cost - if (candies > 0) and junior_evolution_cost: - keep_for_evo = int(candies / (junior_evolution_cost + 1)) - else: - keep_for_evo = 0 + if (candies > 0) and junior_evolution_cost: + keep_for_evo = int(candies / (junior_evolution_cost + 1)) + else: + keep_for_evo = 0 - evo_crap = [p for p in crap if p.has_next_evolution() and p.evolution_cost == junior_evolution_cost][:keep_for_evo] - transfer = [p for p in crap if p not in evo_crap] + evo_crap = [p for p in crap if p.has_next_evolution() and p.evolution_cost == junior_evolution_cost][:keep_for_evo] + transfer = [p for p in crap if p not in evo_crap] + else: + evo_crap = [] + transfer = crap return (transfer, can_evolve_best, evo_crap) @@ -197,15 +212,21 @@ def apply_optimization(self, transfer, evo): for pokemon in transfer: self.transfer_pokemon(pokemon) - if self.config_evolve and self.config_use_lucky_egg: + if len(evo) == 0: + return + + if self.config_evolve and self.config_use_lucky_egg and (not self.bot.config.test): lucky_egg = inventory.items().get(Item.ITEM_LUCKY_EGG.value) # @UndefinedVariable - if lucky_egg.count == 0: - if self.config_evolve_only_with_lucky_egg: - self.logger.info("Skipping evolution step. No lucky egg available") - return - elif len(evo) >= self.config_minimum_evolve_for_lucky_egg: - self.use_lucky_egg() + if self.config_evolve_only_with_lucky_egg and (lucky_egg.count == 0): + self.logger.info("Skipping evolution step. No lucky egg available") + return + + if len(evo) < self.config_minimum_evolve_for_lucky_egg: + self.logger.info("Skipping evolution step. Not enough Pokemons (%s) to evolve", len(evo)) + return + + self.use_lucky_egg() self.logger.info("Evolving %s Pokemons", len(evo)) @@ -213,29 +234,39 @@ def apply_optimization(self, transfer, evo): self.evolve_pokemon(pokemon) def transfer_pokemon(self, pokemon): - if self.config_transfer: - self.bot.api.release_pokemon(pokemon_id=pokemon.id) + if self.config_transfer and (not self.bot.config.test): + response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon.id) else: - pass + response_dict = {"responses": {"RELEASE_POKEMON": {"candy_awarded": 0}}} + + if not response_dict: + return False self.emit_event("pokemon_release", - formatted="Exchanged {pokemon} [IV {iv}] [CP {cp}]", + formatted="Exchanged {pokemon} [IV {iv}] [CP {cp}] [NCP {ncp}] [DPS {dps}]", data={"pokemon": pokemon.name, "iv": pokemon.iv, - "cp": pokemon.cp}) + "cp": pokemon.cp, + "ncp": round(pokemon.ncp, 2), + "dps": round(pokemon.dps, 2)}) + + if self.config_transfer and (not self.bot.config.test): + candy = response_dict.get("responses", {}).get("RELEASE_POKEMON", {}).get("candy_awarded", 0) + + inventory.candies().get(pokemon.pokemon_id).add(candy) + inventory.pokemons().remove(pokemon.id) - if self.config_transfer: - inventory.candies().get(pokemon.pokemon_id).add(1) action_delay(self.bot.config.action_wait_min, self.bot.config.action_wait_max) + return True + def use_lucky_egg(self): lucky_egg = inventory.items().get(Item.ITEM_LUCKY_EGG.value) # @UndefinedVariable - if self.config_evolve and self.config_use_lucky_egg: - response_dict = self.bot.use_lucky_egg() - lucky_egg.remove(1) - else: - response_dict = {"responses": {"USE_ITEM_XP_BOOST": {"result": 1}}} + if lucky_egg.count == 0: + return False + + response_dict = self.bot.use_lucky_egg() if not response_dict: self.emit_event("lucky_egg_error", @@ -246,6 +277,8 @@ def use_lucky_egg(self): result = response_dict.get("responses", {}).get("USE_ITEM_XP_BOOST", {}).get("result", 0) if result == 1: + lucky_egg.remove(1) + self.emit_event("used_lucky_egg", formatted="Used lucky egg ({amount_left} left).", data={"amount_left": lucky_egg.count}) @@ -257,7 +290,7 @@ def use_lucky_egg(self): return False def evolve_pokemon(self, pokemon): - if self.config_evolve: + if self.config_evolve and (not self.bot.config.test): response_dict = self.bot.api.evolve_pokemon(pokemon_id=pokemon.id) else: response_dict = {"responses": {"EVOLVE_POKEMON": {"result": 1}}} @@ -266,20 +299,30 @@ def evolve_pokemon(self, pokemon): return False result = response_dict.get("responses", {}).get("EVOLVE_POKEMON", {}).get("result", 0) + + if result != 1: + return False + xp = response_dict.get("responses", {}).get("EVOLVE_POKEMON", {}).get("experience_awarded", 0) + candy = response_dict.get("responses", {}).get("EVOLVE_POKEMON", {}).get("candy_awarded", 0) + evolution = response_dict.get("responses", {}).get("EVOLVE_POKEMON", {}).get("evolved_pokemon_data", {}) - if result == 1: - self.emit_event("pokemon_evolved", - formatted="Evolved {pokemon} [IV {iv}] [CP {cp}] [+{xp} xp]", - data={"pokemon": pokemon.name, - "iv": pokemon.iv, - "cp": pokemon.cp, - "xp": xp}) + self.emit_event("pokemon_evolved", + formatted="Evolved {pokemon} [IV {iv}] [CP {cp}] [NCP {ncp}] [DPS {dps}] [+{xp} xp]", + data={"pokemon": pokemon.name, + "iv": pokemon.iv, + "cp": pokemon.cp, + "ncp": round(pokemon.ncp, 2), + "dps": round(pokemon.dps, 2), + "xp": xp}) - if self.config_evolve: - inventory.candies().get(pokemon.pokemon_id).consume(pokemon.evolution_cost) - sleep(20) + if self.config_evolve and (not self.bot.config.test): + inventory.candies().get(pokemon.pokemon_id).consume(pokemon.evolution_cost - candy) + inventory.pokemons().remove(pokemon.id) - return True - else: - return False + new_pokemon = inventory.Pokemon(evolution) + inventory.pokemons().add(new_pokemon) + + sleep(20) + + return True diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index a8eb62c34a..f9a6da6fc4 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -159,7 +159,9 @@ def release_pokemon(self, pokemon): data={ 'pokemon': pokemon.name, 'cp': pokemon.cp, - 'iv': pokemon.iv + 'iv': pokemon.iv, + 'ncp': pokemon.cp_percent, + 'dps': pokemon.moveset.dps } ) action_delay(self.bot.config.action_wait_min, self.bot.config.action_wait_max) From d4200e9b34ef6ec9826eb81a6ceb420162b8c674 Mon Sep 17 00:00:00 2001 From: mercuriete Date: Sat, 13 Aug 2016 03:28:42 +0100 Subject: [PATCH 197/202] small fix in VOLUME in Dockerfile (#3779) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 141325d60e..6b71a5c241 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM python:2.7 WORKDIR /usr/src/app -VOLUME ["/usr/app/configs", "/usr/src/app/web"] +VOLUME ["/usr/src/app/configs", "/usr/src/app/web"] ARG timezone=Etc/UTC RUN echo $timezone > /etc/timezone \ From 653ff05f0bf33537aca4aec2f18999567584d125 Mon Sep 17 00:00:00 2001 From: Devin Wendt Date: Sat, 13 Aug 2016 09:08:14 -0500 Subject: [PATCH 198/202] Fix looping between equidstant pokestops (#3787) * Fix looping between equidstant pokestops * :D --- CONTRIBUTORS.md | 1 + pokemongo_bot/cell_workers/move_to_fort.py | 33 +++++++++++++--------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c363f3d804..9ff3c33f4d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -69,3 +69,4 @@ * extink * Quantra * pmquan + * umbreon222 diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 2be287e86c..8fd6c09495 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -7,11 +7,11 @@ from pokemongo_bot.base_task import BaseTask from utils import distance, format_dist, fort_details - class MoveToFort(BaseTask): SUPPORTED_TASK_API_VERSION = 1 def initialize(self): + self.last_nearest_fort = None self.lure_distance = 0 self.lure_attraction = self.config.get("lure_attraction", True) self.lure_max_distance = self.config.get("lure_max_distance", 2000) @@ -96,11 +96,10 @@ def _get_nearest_fort_on_lure_way(self, forts): lures = filter(lambda x: True if x.get('lure_info', None) != None else False, forts) + dist_lure_me = 0 + if (len(lures)): - dist_lure_me = distance(self.bot.position[0], self.bot.position[1], - lures[0]['latitude'],lures[0]['longitude']) - else: - dist_lure_me = 0 + dist_lure_me = self.get_distance_from_bot(lures[0]) if dist_lure_me > 0 and dist_lure_me < self.lure_max_distance: @@ -112,11 +111,7 @@ def _get_nearest_fort_on_lure_way(self, forts): fort['longitude'], lures[0]['latitude'], lures[0]['longitude']) - dist_fort_me = distance( - fort['latitude'], - fort['longitude'], - self.bot.position[0], - self.bot.position[1]) + dist_fort_me = self.get_distance_from_bot(fort) if dist_lure_fort < dist_lure_me and dist_lure_me > dist_fort_me: return fort, dist_lure_me @@ -129,6 +124,9 @@ def _get_nearest_fort_on_lure_way(self, forts): else: return None, 0 + def get_distance_from_bot(self, fort): + return distance(self.bot.position[0], self.bot.position[1], fort['latitude'], fort['longitude']) + def get_nearest_fort(self): forts = self.bot.get_forts(order_by_distance=True) @@ -146,7 +144,16 @@ def get_nearest_fort(self): if (lure_distance > 0): return next_attracted_pts - if len(forts) > 0: - return forts[0] - else: + if len(forts) <= 0: return None + + nearest_fort = forts[0] + + # If last fort moved to and nearest fort are equidistant keep walking + # toward the last fort to avoid looping + if self.last_nearest_fort is not None and self.get_distance_from_bot(self.last_nearest_fort) == self.get_distance_from_bot(nearest_fort): + nearest_fort = self.last_nearest_fort + + self.last_nearest_fort = nearest_fort + + return nearest_fort From 1817d008db17c79447763c9708bb402526a5d338 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sat, 13 Aug 2016 07:14:09 -0700 Subject: [PATCH 199/202] Revert "Fix looping between equidstant pokestops" (#3848) --- CONTRIBUTORS.md | 1 - pokemongo_bot/cell_workers/move_to_fort.py | 33 +++++++++------------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 9ff3c33f4d..c363f3d804 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -69,4 +69,3 @@ * extink * Quantra * pmquan - * umbreon222 diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 8fd6c09495..2be287e86c 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -7,11 +7,11 @@ from pokemongo_bot.base_task import BaseTask from utils import distance, format_dist, fort_details + class MoveToFort(BaseTask): SUPPORTED_TASK_API_VERSION = 1 def initialize(self): - self.last_nearest_fort = None self.lure_distance = 0 self.lure_attraction = self.config.get("lure_attraction", True) self.lure_max_distance = self.config.get("lure_max_distance", 2000) @@ -96,10 +96,11 @@ def _get_nearest_fort_on_lure_way(self, forts): lures = filter(lambda x: True if x.get('lure_info', None) != None else False, forts) - dist_lure_me = 0 - if (len(lures)): - dist_lure_me = self.get_distance_from_bot(lures[0]) + dist_lure_me = distance(self.bot.position[0], self.bot.position[1], + lures[0]['latitude'],lures[0]['longitude']) + else: + dist_lure_me = 0 if dist_lure_me > 0 and dist_lure_me < self.lure_max_distance: @@ -111,7 +112,11 @@ def _get_nearest_fort_on_lure_way(self, forts): fort['longitude'], lures[0]['latitude'], lures[0]['longitude']) - dist_fort_me = self.get_distance_from_bot(fort) + dist_fort_me = distance( + fort['latitude'], + fort['longitude'], + self.bot.position[0], + self.bot.position[1]) if dist_lure_fort < dist_lure_me and dist_lure_me > dist_fort_me: return fort, dist_lure_me @@ -124,9 +129,6 @@ def _get_nearest_fort_on_lure_way(self, forts): else: return None, 0 - def get_distance_from_bot(self, fort): - return distance(self.bot.position[0], self.bot.position[1], fort['latitude'], fort['longitude']) - def get_nearest_fort(self): forts = self.bot.get_forts(order_by_distance=True) @@ -144,16 +146,7 @@ def get_nearest_fort(self): if (lure_distance > 0): return next_attracted_pts - if len(forts) <= 0: + if len(forts) > 0: + return forts[0] + else: return None - - nearest_fort = forts[0] - - # If last fort moved to and nearest fort are equidistant keep walking - # toward the last fort to avoid looping - if self.last_nearest_fort is not None and self.get_distance_from_bot(self.last_nearest_fort) == self.get_distance_from_bot(nearest_fort): - nearest_fort = self.last_nearest_fort - - self.last_nearest_fort = nearest_fort - - return nearest_fort From 6ffc8c01978b5ab0a47826fe3dc852789524e5d8 Mon Sep 17 00:00:00 2001 From: Vianney Dubus Date: Sat, 13 Aug 2016 18:06:53 +0200 Subject: [PATCH 200/202] configuration_files.md - Update nicknaming sample usages (#3833) * Format iv_pct on 3 digits For better sorting on pokemon's name, format iv_pct on 3 digits. * Fix Nicknaming documentation sample usage Fix some nicknaming samble usage around the iv_pct which it now formatted on 3 digits. --- docs/configuration_files.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration_files.md b/docs/configuration_files.md index 42b4feb069..5a1bd942b1 100644 --- a/docs/configuration_files.md +++ b/docs/configuration_files.md @@ -251,8 +251,8 @@ Key | Info > **NOTE:** Use a blank template (`""`) to revert all pokemon to their original names (as if they had no nickname). Sample usages: -- `"{name}_{iv_pct}"` => `Mankey_69` -- `"{iv_pct}_{iv_ads}"` => `91_15/11/15` +- `"{name}_{iv_pct}"` => `Mankey_069` +- `"{iv_pct}_{iv_ads}"` => `091_15/11/15` - `""` -> `Mankey` - `"{attack_code}{attack_pct1}{defense_pct1}{ivcp_pct1}{name}"` => `Lh474Golbat` ![sample](https://cloud.githubusercontent.com/assets/8896778/17285954/0fa44a88-577b-11e6-8204-b1302f4294bd.png) From dbf26d556fcaf4dfb6a27616ce749b98f15a614b Mon Sep 17 00:00:00 2001 From: Brice Date: Sat, 13 Aug 2016 18:07:15 +0200 Subject: [PATCH 201/202] Now track inventory when spinning a fort (#3774) * Ignoring compiled test python file of PyCharm * Now spinning fort keeps track of cached inventory * Now the pokemon_catch_worker keeps track of cached ITEMS (and only items, not pokemon) inventory * Minor improvements of the new inventory * Fixed key error * Minor improvements of the new inventory * Fixed attribute non existent * Removed duplicated import --- .gitignore | 1 + pokemongo_bot/cell_workers/move_to_fort.py | 3 +- pokemongo_bot/cell_workers/recycle_items.py | 2 +- pokemongo_bot/cell_workers/spin_fort.py | 62 ++++++++++++++------- pokemongo_bot/inventory.py | 60 +++++++++++++------- 5 files changed, 85 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index cfba4942e0..3c504a3774 100644 --- a/.gitignore +++ b/.gitignore @@ -101,6 +101,7 @@ share/ # PyCharm IDE settings .idea/ *.iml +out/ # Personal load details src/ diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 2be287e86c..4d0ff4896f 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from pokemongo_bot import inventory from pokemongo_bot.constants import Constants from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult @@ -18,7 +19,7 @@ def initialize(self): 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() + has_space_for_loot = inventory.Items.has_space_for_loot() if not has_space_for_loot and not self.ignore_item_count: self.emit_event( 'inventory_full', diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 9fa2518e35..e48dff3cd8 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -67,7 +67,7 @@ def should_run(self): :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): + 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 diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 0ba09ca633..eee88525c5 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -6,6 +6,7 @@ import time from pgoapi.utilities import f2i +from pokemongo_bot import inventory from pokemongo_bot.constants import Constants from pokemongo_bot.human_behaviour import sleep @@ -14,6 +15,11 @@ from pokemongo_bot.base_dir import _base_dir from utils import distance, format_time, fort_details +SPIN_REQUEST_RESULT_SUCCESS = 1 +SPIN_REQUEST_RESULT_OUT_OF_RANGE = 2 +SPIN_REQUEST_RESULT_IN_COOLDOWN_PERIOD = 3 +SPIN_REQUEST_RESULT_INVENTORY_FULL = 4 + class SpinFort(BaseTask): SUPPORTED_TASK_API_VERSION = 1 @@ -22,12 +28,13 @@ def initialize(self): self.ignore_item_count = self.config.get("ignore_item_count", False) def should_run(self): - if not self.bot.has_space_for_loot() and not self.ignore_item_count: + has_space_for_loot = inventory.Items.has_space_for_loot() + if not has_space_for_loot and not self.ignore_item_count: self.emit_event( 'inventory_full', formatted="Inventory is full. You might want to change your config to recycle more items if this message appears consistently." ) - return self.ignore_item_count or self.bot.has_space_for_loot() + return self.ignore_item_count or has_space_for_loot def work(self): forts = self.get_forts_in_range() @@ -50,25 +57,16 @@ def work(self): 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']: + if 'responses' in response_dict and 'FORT_SEARCH' in response_dict['responses']: spin_details = response_dict['responses']['FORT_SEARCH'] spin_result = spin_details.get('result', -1) - if spin_result == 1: + if spin_result == SPIN_REQUEST_RESULT_SUCCESS: self.bot.softban = False experience_awarded = spin_details.get('experience_awarded', 0) - items_awarded = spin_details.get('items_awarded', {}) - if items_awarded: - self.bot.latest_inventory = None - tmp_count_items = {} - for item in items_awarded: - item_id = item['item_id'] - item_name = self.bot.item_list[str(item_id)] - if not item_name in tmp_count_items: - tmp_count_items[item_name] = item['item_count'] - else: - tmp_count_items[item_name] += item['item_count'] + + + items_awarded = self.get_items_awarded_from_fort_spinned(response_dict) if experience_awarded or items_awarded: self.emit_event( @@ -77,7 +75,7 @@ def work(self): data={ 'pokestop': fort_name, 'exp': experience_awarded, - 'items': tmp_count_items + 'items': items_awarded } ) else: @@ -90,13 +88,13 @@ def work(self): 'cooldown_complete_timestamp_ms') self.bot.fort_timeouts.update({fort["id"]: pokestop_cooldown}) self.bot.recent_forts = self.bot.recent_forts[1:] + [fort['id']] - elif spin_result == 2: + elif spin_result == SPIN_REQUEST_RESULT_OUT_OF_RANGE: self.emit_event( 'pokestop_out_of_range', formatted="Pokestop {pokestop} out of range.", data={'pokestop': fort_name} ) - elif spin_result == 3: + elif spin_result == SPIN_REQUEST_RESULT_IN_COOLDOWN_PERIOD: pokestop_cooldown = spin_details.get( 'cooldown_complete_timestamp_ms') if pokestop_cooldown: @@ -110,7 +108,7 @@ def work(self): formatted="Pokestop {pokestop} on cooldown. Time left: {minutes_left}.", data={'pokestop': fort_name, 'minutes_left': minutes_left} ) - elif spin_result == 4: + elif spin_result == SPIN_REQUEST_RESULT_INVENTORY_FULL: if not self.ignore_item_count: self.emit_event( 'inventory_full', @@ -165,3 +163,27 @@ def get_forts_in_range(self): ) <= Constants.MAX_DISTANCE_FORT_IS_REACHABLE, forts) return forts + + def get_items_awarded_from_fort_spinned(self, response_dict): + items_awarded = response_dict['responses']['FORT_SEARCH'].get('items_awarded', {}) + if items_awarded: + tmp_count_items = {} + for item_awarded in items_awarded: + + item_awarded_id = item_awarded['item_id'] + item_awarded_name = inventory.Items.name_for(item_awarded_id) + item_awarded_count = item_awarded['item_count'] + + if not item_awarded_name in tmp_count_items: + tmp_count_items[item_awarded_name] = item_awarded_count + else: + tmp_count_items[item_awarded_name] += item_awarded_count + + self._update_inventory(item_awarded) + + return tmp_count_items + + # TODO : Refactor this class, hide the inventory update right after the api call + def _update_inventory(self, item_awarded): + inventory.items().get(item_awarded['item_id']).add(item_awarded['item_count']) + diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index f314687a13..ccac6f43be 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -106,6 +106,25 @@ 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 + + def __str__(self): + return self.name + " : " + str(self.count) + class Items(_BaseInventoryComponent): TYPE = 'item' @@ -140,11 +159,23 @@ def get_space_used(cls): def get_space_left(cls): """ Compute the space left in item inventory. - :return: The space left in item inventory. + :return: The space left in item inventory. 0 if the player has more item than his item inventory can carry. :rtype: int """ _inventory.retrieve_item_inventory_size() - return _inventory.item_inventory_size - cls.get_space_used() + space_left = _inventory.item_inventory_size - cls.get_space_used() + # Space left should never be negative. Returning 0 if the computed value is negative. + return space_left if space_left >= 0 else 0 + + @classmethod + def has_space_for_loot(cls): + """ + Returns a value indicating whether or not the item inventory has enough space to loot a fort + :return: True if the item inventory has enough space; otherwise, False. + :rtype: bool + """ + max_number_of_items_looted_at_stop = 5 + return cls.get_space_left() >= max_number_of_items_looted_at_stop class Pokemons(_BaseInventoryComponent): @@ -479,23 +510,6 @@ def add(self, amount): self.quantity += amount -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 Egg(object): def __init__(self, data): self._data = data @@ -1004,6 +1018,7 @@ 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 @@ -1087,12 +1102,10 @@ 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 @@ -1110,6 +1123,11 @@ def pokemons(refresh=False): def items(): + """ + Access to the cached item inventory + :return: Instance of the cached item inventory + :rtype: Items + """ return _inventory.items From 1e22ae1b128a653caa6cfb55de89035c13091bbc Mon Sep 17 00:00:00 2001 From: Zhou Date: Mon, 1 Aug 2016 12:46:11 -0700 Subject: [PATCH 202/202] Evolve only if evolvable candidates no less than a certain number. Previously, when user configured use_lucky_egg as true, the bot will use lucky egg even evolvale candidates is zero. That's kinda of waste. Meanwhile, if user didn't configured use_lucky_egg, the bot will try to evolve after sort by cp&iv, but sometimes no good potential pokemon in that batch, it's better delay the evolution after a while. By adding a config item evovle_num_min can cope above two problems. The bot evolves only if evolvable candidates no less than a certain number. 1. Evolve only if evolvable candidates no less than a certain number. 2. Dragoniar's configuration is abnormal in pokemon.json, which lack of "Previous evolution(s)" attribute, while the other middle-tier pokemon has. Fixed some issues after: Refactor evolve_all worker #2244 1. The refactoring try to sort by pokemon index desc which is not good. For example, why Venusaur which is #003 should be with lower priority than Pidgey(#016)? 2. The refactoring always use dict.get(key, {}).get(key, {}) which will eventually return a {} which will cause side effect. It's better fail fast when the dict has no attribute which is unexpected by the developer. 3. The reafctoring try to use cache when the pokemon is not evovlable. That developer doesn't know previously we didn't caculate the candy before evovling, thus caused a lot of failure when evovling. After caculating candy requirements by the refactoring and this patch, the failure rarely happens. Thus cache is not necessary when the pokemon is not evovlable. --- configs/config.json.cluster.example | 1 + configs/config.json.example | 1 + configs/config.json.path.example | 1 + configs/config.json.pokemon.example | 1 + pokemongo_bot/cell_workers/evolve_pokemon.py | 80 +++++++++++++------- 5 files changed, 58 insertions(+), 26 deletions(-) diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index 1bedbcbab9..5d4c741e35 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -48,6 +48,7 @@ "first_evolve_by": "cp", "evolve_above_cp": 500, "evolve_above_iv": 0.8, + "evolve_num_min": 5, "logic": "or", "evolve_speed": 20, "use_lucky_egg": false diff --git a/configs/config.json.example b/configs/config.json.example index 838acc1d4b..3576bf2a73 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -55,6 +55,7 @@ "first_evolve_by": "cp", "evolve_above_cp": 500, "evolve_above_iv": 0.8, + "evolve_num_min": 5, "logic": "or", "evolve_speed": 20, "use_lucky_egg": false diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 2581862b31..254eff6be5 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -48,6 +48,7 @@ "first_evolve_by": "cp", "evolve_above_cp": 500, "evolve_above_iv": 0.8, + "evolve_num_min": 5, "logic": "or", "evolve_speed": 20, "use_lucky_egg": false diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 2ad81a7369..c656a0a6c2 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -48,6 +48,7 @@ "first_evolve_by": "cp", "evolve_above_cp": 500, "evolve_above_iv": 0.8, + "evolve_num_min": 5, "logic": "or", "evolve_speed": 20, "use_lucky_egg": false diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 4de40fdb7d..13299f0ed8 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -15,6 +15,7 @@ def initialize(self): self.first_evolve_by = self.config.get('first_evolve_by', 'cp') self.evolve_above_cp = self.config.get('evolve_above_cp', 500) self.evolve_above_iv = self.config.get('evolve_above_iv', 0.8) + self.evolve_num_min = self.config.get('evolve_num_min', 5) self.cp_iv_logic = self.config.get('logic', 'or') self.use_lucky_egg = self.config.get('use_lucky_egg', False) self._validate_config() @@ -27,29 +28,26 @@ def work(self): if not self._should_run(): return - 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) - - cache = {} - for pokemon in evolve_list: - if pokemon.can_evolve_now(): - self._execute_pokemon_evolve(pokemon, cache) + cache = set() - def _should_run(self): - if not self.evolve_all or self.evolve_all[0] == 'none': - return False + evolve_list = self._sort_and_filter() + # filter out non-listed pokemons, top-tier pokemons and those with not enough candy + evolve_list = [x for x in evolve_list if self._is_evolvable(x, inventory.candies().deepcopy())] - # Evolve all is used - Use Lucky egg only at the first tick - if self.bot.tick_count is not 1 or not self.use_lucky_egg: - return True + # Don't evolve unless the evolvable candidates number is no less than evolve_num_min + if len(evolve_list) < self.evolve_num_min: + return - lucky_egg_count = self.bot.item_inventory_count(Item.ITEM_LUCKY_EGG.value) + if self.use_lucky_egg: + lucky_egg_count = self.bot.item_inventory_count(Item.ITEM_LUCKY_EGG.value) + # Sometimes remaining lucky egg count get changed, check again for sure + if lucky_egg_count <= 0: + self.emit_event( + 'skip_evolve', + formatted='Skipping evolve because has no lucky egg.' + ) + return - # Make sure the user has a lucky egg and skip if not - if lucky_egg_count > 0: response_dict_lucky_egg = self.bot.use_lucky_egg() if response_dict_lucky_egg: result = response_dict_lucky_egg.get('responses', {}).get('USE_ITEM_XP_BOOST', {}).get('result', 0) @@ -58,32 +56,45 @@ def _should_run(self): 'used_lucky_egg', formatted='Used lucky egg ({amount_left} left).', data={ - 'amount_left': lucky_egg_count - 1 + 'amount_left': lucky_egg_count - 1 } ) - return True else: self.emit_event( 'lucky_egg_error', level='error', formatted='Failed to use lucky egg!' ) - return False - else: - # Skipping evolve so they aren't wasted + return + + for pokemon in evolve_list: + self._execute_pokemon_evolve(pokemon, cache) + + def _should_run(self): + # Don't run after the first tick + # Lucky Egg should only be popped at the first tick + if not self.evolve_all or self.evolve_all[0] == 'none' or self.bot.tick_count is 1: + return False + + # Will skip evolving if user wants to use an egg and there is none + lucky_egg_count = self.bot.item_inventory_count(Item.ITEM_LUCKY_EGG.value) + if self.use_lucky_egg and lucky_egg_count <= 0: self.emit_event( 'skip_evolve', formatted='Skipping evolve because has no lucky egg.' ) + return False + # Otherwise try evolving + return True + 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 } - 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) @@ -95,6 +106,22 @@ def _sort_and_filter(self): return pokemons + + def _is_evolvable(self, pokemon, candies): + # filter out non-listed pokemen + if self.evolve_all[0] != 'all' and pokemon.name not in self.evolve_all: + return False + + if not pokemon.has_seen_next_evolution(): + return False + + if candies.get(pokemon.pokemon_id).quantity >= pokemon.evolution_cost: + candies.get(pokemon.pokemon_id).consume(pokemon.evolution_cost) + return True + # not enough candies + return False + + def _execute_pokemon_evolve(self, pokemon, cache): if pokemon.name in cache: return False @@ -118,10 +145,11 @@ def _execute_pokemon_evolve(self, pokemon, cache): inventory.pokemons().remove(pokemon.id) pokemon = Pokemon(response_dict.get('responses', {}).get('EVOLVE_POKEMON', {}).get('evolved_pokemon_data', {})) inventory.pokemons().add(pokemon) + sleep(self.evolve_speed) return True else: # cache pokemons we can't evolve. Less server calls - cache[pokemon.name] = 1 + cache.add(pokemon.name) sleep(0.7) return False