diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 3091852955..e5a3f1f317 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,12 +1,23 @@
+## 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
### 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 a12509c322..3c504a3774 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
@@ -95,6 +100,8 @@ ENV/
# PyCharm IDE settings
.idea/
+*.iml
+out/
# Personal load details
src/
@@ -113,6 +120,7 @@ configs/*
!configs/config.json.map.example
!configs/path.example.json
!config.json.cluster.example
+!config.json.optimizer.example
# Virtualenv folders
bin/
@@ -120,3 +128,6 @@ include/
# Pip check file
pip-selfcheck.json
+
+# Some love for the vim users
+.*.sw*
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 bd7894f0c2..c363f3d804 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -50,3 +50,22 @@
* z4ppy.bbc
* matheussampaio
* Abraxas000
+ * lucasfevi
+ * pokepal
+ * Moonlight-Angel
+ * mjmadsen
+ * nikofil
+ * bigkraig
+ * nikhil-pandey
+ * thebigjc
+ * JaapMoolenaar
+ * eevee-github
+ * g0vanish
+ * cmezh
+ * Nivong
+ * kestel
+ * simonsmh
+ * joaodragao
+ * extink
+ * Quantra
+ * pmquan
diff --git a/Dockerfile b/Dockerfile
index 58c45cd02f..6b71a5c241 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,13 +1,26 @@
-FROM python:2.7-onbuild
+FROM python:2.7
+
+WORKDIR /usr/src/app
+VOLUME ["/usr/src/app/configs", "/usr/src/app/web"]
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
+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 \
+ && rm -rf /tmp/pgoencrypt*
+
+ENV LD_LIBRARY_PATH /usr/src/app
+
+COPY requirements.txt /usr/src/app/
+RUN pip install --no-cache-dir -r requirements.txt
-VOLUME ["/usr/src/app/web"]
+COPY . /usr/src/app
-ENTRYPOINT ["python", "pokecli.py"]
\ No newline at end of file
+ENTRYPOINT ["python", "pokecli.py"]
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
diff --git a/README.md b/README.md
index 6fa12d4f8a..1c0fa41910 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,15 @@
# 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`.
-We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com)
+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`.
+
+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)
+- [Documentation](https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/)
- [Features](#features)
-- [Wiki](#wiki)
- [Credits](#credits)
-- [Donation](#donation)
-
## Features
- [x] GPS Location configuration
@@ -23,37 +23,34 @@ We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](
- [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
## 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.
-## 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.
+## 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`.
+
+## Help Needed on [Desktop Version](https://github.com/PokemonGoF/PokemonGo-Bot-Desktop)
+
## 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
- [AHAAAAAAA](https://github.com/AHAAAAAAA/PokemonGo-Map) for parts of the s2sphere stuff
-[](https://github.com/igrigorik/ga-beacon)
+[](https://github.com/igrigorik/ga-beacon)
diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example
index 5613061a84..5d4c741e35 100644
--- a/configs/config.json.cluster.example
+++ b/configs/config.json.cluster.example
@@ -4,10 +4,21 @@
"password": "YOUR_PASSWORD",
"location": "SOME_LOCATION",
"gmapkey": "GOOGLE_MAPS_API_KEY",
+ "encrypt_location": "",
"tasks": [
{
"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"
},
@@ -17,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"
},
@@ -27,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
@@ -35,6 +57,7 @@
{
"type": "RecycleItems",
"config": {
+ "min_empty_space": 15,
"item_filter": {
"Pokeball": { "keep" : 100 },
"Potion": { "keep" : 10 },
@@ -62,7 +85,6 @@
}
}
],
- "max_steps": 5,
"forts": {
"avoid_circles": true,
"max_circle_size": 50
@@ -77,9 +99,10 @@
"location_cache": true,
"distance_unit": "km",
"reconnecting_timeout": 15,
- "evolve_captured": "NONE",
"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"},
"// Example of always catching Rattata:": {},
diff --git a/configs/config.json.example b/configs/config.json.example
index 20ef72e34e..3576bf2a73 100644
--- a/configs/config.json.example
+++ b/configs/config.json.example
@@ -4,10 +4,21 @@
"password": "YOUR_PASSWORD",
"location": "SOME_LOCATION",
"gmapkey": "GOOGLE_MAPS_API_KEY",
+ "encrypt_location": "",
"tasks": [
{
"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"
},
@@ -17,9 +28,26 @@
"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"
},
+ {
+ "type": "NicknamePokemon",
+ "config": {
+ "enabled": false,
+ "nickname_template": "{iv_pct}_{iv_ads}"
+ }
+ },
{
"type": "EvolvePokemon",
"config": {
@@ -27,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
@@ -35,6 +64,7 @@
{
"type": "RecycleItems",
"config": {
+ "min_empty_space": 15,
"item_filter": {
"Pokeball": { "keep" : 100 },
"Potion": { "keep" : 10 },
@@ -72,7 +102,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,
@@ -84,14 +115,22 @@
"location_cache": true,
"distance_unit": "km",
"reconnecting_timeout": 15,
- "evolve_captured": "NONE",
"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"},
"// 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/configs/config.json.map.example b/configs/config.json.map.example
index 6436b95905..6051e063cc 100644
--- a/configs/config.json.map.example
+++ b/configs/config.json.map.example
@@ -4,10 +4,21 @@
"password": "YOUR_PASSWORD",
"location": "SOME_LOCATION",
"gmapkey": "GOOGLE_MAPS_API_KEY",
+ "encrypt_location": "",
"tasks": [
{
"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"
},
@@ -17,11 +28,21 @@
"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"
},
{
- "type": "EvolveAll",
+ "type": "EvolvePokemon",
"config": {
"evolve_all": "NONE",
"evolve_cp_min": 300,
@@ -32,6 +53,7 @@
{
"type": "RecycleItems",
"config": {
+ "min_empty_space": 15,
"item_filter": {
"Pokeball": { "keep" : 100 },
"Potion": { "keep" : 10 },
@@ -57,10 +79,14 @@
"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",
+ "map_path": "raw_data",
"catch": {
"==========Legendaries==========": 0,
"Aerodactyl": 1000,
@@ -300,7 +326,6 @@
}
],
"map_object_cache_time": 5,
- "max_steps": 5,
"forts": {
"avoid_circles": true,
"max_circle_size": 50
@@ -315,9 +340,10 @@
"location_cache": true,
"distance_unit": "km",
"reconnecting_timeout": 15,
- "evolve_captured": "NONE",
"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"},
"// Example of always catching Rattata:": {},
diff --git a/configs/config.json.optimizer.example b/configs/config.json.optimizer.example
new file mode 100644
index 0000000000..2e91f525d8
--- /dev/null
+++ b/configs/config.json.optimizer.example
@@ -0,0 +1,156 @@
+{
+ "auth_service": "google",
+ "username": "YOUR_USERNAME",
+ "password": "YOUR_PASSWORD",
+ "location": "SOME_LOCATION",
+ "gmapkey": "GOOGLE_MAPS_API_KEY",
+ "encrypt_location": "",
+ "tasks": [
+ {
+ "type": "HandleSoftBan"
+ },
+ {
+ "type": "CollectLevelUpReward"
+ },
+ {
+ "type": "IncubateEggs",
+ "config": {
+ "longer_eggs_first": true
+ }
+ },
+ {
+ "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,
+ "// 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"]
+ }
+ ]
+ }
+ },
+ {
+ "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": true,
+ "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,
+ "min_ultraball_to_keep": 10,
+ "logging_color": true,
+ "catch": {
+ "any": {
+ "always_catch": true
+ }
+ }
+}
diff --git a/configs/config.json.path.example b/configs/config.json.path.example
index 38baa9f1f0..254eff6be5 100644
--- a/configs/config.json.path.example
+++ b/configs/config.json.path.example
@@ -4,10 +4,21 @@
"password": "YOUR_PASSWORD",
"location": "SOME_LOCATION",
"gmapkey": "GOOGLE_MAPS_API_KEY",
+ "encrypt_location": "",
"tasks": [
{
"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"
},
@@ -17,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"
},
@@ -27,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
@@ -35,6 +57,7 @@
{
"type": "RecycleItems",
"config": {
+ "min_empty_space": 15,
"item_filter": {
"Pokeball": { "keep" : 100 },
"Potion": { "keep" : 10 },
@@ -58,12 +81,12 @@
"type": "FollowPath",
"config": {
"path_mode": "loop",
+ "path_start_mode": "first",
"path_file": "configs/path.example.json"
}
}
],
"map_object_cache_time": 5,
- "max_steps": 5,
"forts": {
"avoid_circles": true,
"max_circle_size": 50
@@ -78,9 +101,10 @@
"location_cache": true,
"distance_unit": "km",
"reconnecting_timeout": 15,
- "evolve_captured": "NONE",
"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"},
"// Example of always catching Rattata:": {},
diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example
index 7cad1ac066..c656a0a6c2 100644
--- a/configs/config.json.pokemon.example
+++ b/configs/config.json.pokemon.example
@@ -4,10 +4,21 @@
"password": "YOUR_PASSWORD",
"location": "SOME_LOCATION",
"gmapkey": "GOOGLE_MAPS_API_KEY",
+ "encrypt_location": "",
"tasks": [
{
"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"
},
@@ -17,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"
},
@@ -27,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
@@ -35,6 +57,7 @@
{
"type": "RecycleItems",
"config": {
+ "min_empty_space": 15,
"item_filter": {
"Pokeball": { "keep" : 100 },
"Potion": { "keep" : 10 },
@@ -84,9 +107,10 @@
"location_cache": true,
"distance_unit": "km",
"reconnecting_timeout": 15,
- "evolve_captured": "NONE",
"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/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/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/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/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
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..5a1bd942b1
--- /dev/null
+++ b/docs/configuration_files.md
@@ -0,0 +1,310 @@
+## 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
+ * `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
+* 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:
+
+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).
+
+Sample usages:
+- `"{name}_{iv_pct}"` => `Mankey_069`
+- `"{iv_pct}_{iv_ads}"` => `091_15/11/15`
+- `""` -> `Mankey`
+- `"{attack_code}{attack_pct1}{defense_pct1}{ivcp_pct1}{name}"` => `Lh474Golbat`
+
+
+## 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 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
+```
+{
+ \\ ...
+ {
+ "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..11f8634744
--- /dev/null
+++ b/docs/docker.md
@@ -0,0 +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 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.
+
+```
+cd PokemonGo-Bot
+docker build --build-arg timezone=Europe/London -t pokemongo-bot .
+```
+
+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:
+
+```
+docker images
+```
+
+To run PokemonGo-Bot Docker image you've created:
+
+```
+docker run --name=bot1-pokego --rm -it -v $(pwd)/configs/config.json:/usr/src/app/configs/config.json pokemongo-bot
+```
+
+Run a second container provided with the OpenPoGoBotWeb view:
+
+```
+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 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)
+
+```
+docker-compose up
+```
+
+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!
diff --git a/docs/faq.md b/docs/faq.md
new file mode 100644
index 0000000000..ba2524bbce
--- /dev/null
+++ b/docs/faq.md
@@ -0,0 +1,54 @@
+### How do I start the application?
+After [installing] (https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/installation.md), in the root folder run the following command:
+### Linux
+```
+run.sh
+```
+### Windows
+```
+run.bat
+```
+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..3f68512d1b
--- /dev/null
+++ b/docs/installation.md
@@ -0,0 +1,209 @@
+### 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)
+
+#Linux/Mac Automatic installation
+### Easy installation
+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`
+ This will reset and makes sure you have no changes made to any code since it will overide it
+3. Rerun the bot `./run.sh`
+
+note: we do not support windows at this time
+
+
+# Manual installation
+### 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.
+
+### Get encrypt.so (Windows part writing need fine tune)
+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 && 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.
+
+## 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`
+
+### Linux Installation
+####on the Example of Ubuntu
+(change dev to master for the lastest master version)
+
+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
+(change master to dev for the latest version)
+
+```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
+(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/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/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.
+
+
+
+**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
deleted file mode 100755
index c11992bb0c..0000000000
--- a/install.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/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 7df9369120..064c87c9ba
--- a/pokecli.py
+++ b/pokecli.py
@@ -33,60 +33,144 @@
import ssl
import sys
import time
+import signal
from datetime import timedelta
from getpass import getpass
from pgoapi.exceptions import NotLoggedInException, ServerSideRequestThrottlingException, ServerBusyOrOfflineException
from geopy.exc import GeocoderQuotaExceeded
from pokemongo_bot import PokemonGoBot, TreeConfigBuilder
-from pokemongo_bot import logger
+from pokemongo_bot.base_dir import _base_dir
+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
+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')
- 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')
+class SIGINTRecieved(Exception): pass
- finished = False
+def main():
+ bot = False
- while not finished:
- try:
- bot = PokemonGoBot(config)
- bot.start()
- tree = TreeConfigBuilder(bot, config.raw_tasks).build()
- bot.workers = tree
- bot.metrics.capture_stats()
+ 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)
+
+ 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.health_record = health_record
+
+ bot.event_manager.emit(
+ 'bot_start',
+ sender=bot,
+ level='info',
+ formatted='Starting bot...'
+ )
+
+ while True:
+ bot.tick()
+
+ except (KeyboardInterrupt, SIGINTRecieved):
+ 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',
+ formatted='Log logged in, reconnecting in {:d}'.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)
- logger.log('Starting PokemonGo Bot....', 'green')
+ 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}
+ )
- while True:
- bot.tick()
- except KeyboardInterrupt:
- logger.log('Exiting PokemonGo Bot', 'red')
- 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 ServerSideRequestThrottlingException:
- logger.log('Server is throttling, reconnecting in 30sec')
- time.sleep(30)
- except GeocoderQuotaExceeded:
- logger.log('[x] The given maps api key has gone over the requests limit.', 'red')
- finished = True
- except:
- # always report session summary and then raise exception
- report_summary(bot)
- raise
def report_summary(bot):
if bot.metrics.start_time is None:
@@ -94,43 +178,54 @@ 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()
- 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
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.log('No config argument specified, checking for /configs/config.json', 'yellow')
- with open(config_file) as data:
- load.update(json.load(data))
+ logger.info('No config argument specified, checking for /configs/config.json')
+ _json_loader(config_file)
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 +323,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,
@@ -256,15 +359,6 @@ def init_config():
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,
@@ -304,26 +398,72 @@ def init_config():
add_config(
parser,
load,
- long_flag="--catch_randomize_reticle_factor",
- help="Randomize factor for pokeball throwing accuracy (DEFAULT 1.0 means no randomize: always 'Excellent' throw. 0.0 randomizes between normal and 'Excellent' throw)",
+ 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,
+ long_flag="--map_object_cache_time",
+ help="Amount of seconds to keep the map object in cache (bypass Niantic throttling)",
type=float,
- default=1.0
+ default=5.0
+ )
+ add_config(
+ parser,
+ load,
+ long_flag="--logging_color",
+ help="If logging_color is set to true, colorized logging handler will be used",
+ type=bool,
+ default=True
)
add_config(
parser,
load,
- long_flag="--catch_randomize_spin_factor",
- help="Randomize factor for pokeball curve throwing (DEFAULT 1.0 means no randomize: always perfect 'Super Spin' curve ball. 0.0 randomizes between normal and 'Super Spin' curve ball)",
+ short_flag="-cte",
+ long_flag="--catch_throw_parameters.excellent_rate",
+ help="Define the odd of performing an excellent throw",
type=float,
- default=1.0
+ default=1
)
add_config(
parser,
load,
- long_flag="--map_object_cache_time",
- help="Amount of seconds to keep the map object in cache (bypass Niantic throttling)",
+ short_flag="-ctg",
+ long_flag="--catch_throw_parameters.great_rate",
+ help="Define the odd of performing a great throw",
type=float,
- default=5.0
+ default=0
+ )
+ add_config(
+ parser,
+ load,
+ short_flag="-ctn",
+ long_flag="--catch_throw_parameters.nice_rate",
+ help="Define the odd of performing a nice throw",
+ type=float,
+ default=0
+ )
+ add_config(
+ parser,
+ load,
+ short_flag="-ctm",
+ long_flag="--catch_throw_parameters.normal_rate",
+ help="Define the odd of performing a normal throw",
+ type=float,
+ default=0
+ )
+ add_config(
+ parser,
+ load,
+ short_flag="-cts",
+ long_flag="--catch_throw_parameters.spin_success_rate",
+ help="Define the odds of performing a spin throw (Value between 0 (never) and 1 (always))",
+ type=float,
+ default=1
)
# Start to parse other attrs
@@ -333,11 +473,14 @@ 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)
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', {})
@@ -373,24 +516,16 @@ 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.")
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)
# create web dir if not exists
try:
@@ -399,9 +534,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) for pokemon_name in config.evolve_captured.split(',')]
-
fix_nested_config(config)
return config
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
diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py
index c5cf3d4614..ff5257c043 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
@@ -8,23 +9,31 @@
import re
import sys
import time
+import Queue
+import threading
from geopy.geocoders import GoogleV3
from pgoapi import PGoApi
from pgoapi.utilities import f2i, get_cell_ids
import cell_workers
-import logger
+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
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, TreeConfigBuilder
+from tree_config_builder import ConfigException, MismatchTaskApiVersion, TreeConfigBuilder
+from inventory import init_inventory
+from sys import platform as _platform
+import struct
class PokemonGoBot(object):
@@ -36,13 +45,22 @@ 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()
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
@@ -52,36 +70,408 @@ 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 = []
+ # 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()
self._setup_api()
+ self._load_recent_forts()
random.seed()
def _setup_event_system(self):
- handlers = [LoggingHandler()]
- if self.config.websocket_server:
- websocket_handler = SocketIoHandler(self.config.websocket_server_url)
- handlers.append(websocket_handler)
+ 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)
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',
+ 'encounter_id',
+ 'latitude',
+ 'longitude',
+ 'pokemon_id'
+ )
+ )
+ self.event_manager.register_event('no_pokeballs')
+ self.event_manager.register_event(
+ 'pokemon_catch_rate',
+ parameters=(
+ 'catch_rate',
+ 'ball_name',
+ 'berry_name',
+ 'berry_count'
+ )
+ )
+ self.event_manager.register_event(
+ 'threw_berry',
+ parameters=(
+ 'berry_name',
+ 'ball_name',
+ 'new_catch_rate'
+ )
+ )
+ self.event_manager.register_event(
+ 'threw_pokeball',
+ parameters=(
+ 'ball_name',
+ 'success_percentage',
+ 'count_left'
+ )
+ )
+ self.event_manager.register_event(
+ 'pokemon_capture_failed',
+ parameters=('pokemon',)
+ )
+ self.event_manager.register_event(
+ 'pokemon_vanished',
+ 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')
+ self.event_manager.register_event(
+ 'pokemon_caught',
+ parameters=(
+ 'pokemon',
+ 'cp', 'iv', 'iv_display', 'exp',
+ 'encounter_id',
+ 'latitude',
+ 'longitude',
+ 'pokemon_id'
+ )
+ )
+ self.event_manager.register_event(
+ 'pokemon_evolved',
+ 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',))
+ 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(
+ '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_skipped',
+ parameters=('space',)
+ )
+ self.event_manager.register_event(
+ 'item_discard_fail',
+ parameters=('item',)
+ )
- # 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}),
+ # 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', 'iv', 'cp', 'ncp', 'dps')
+ )
+
+ # 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',
+ parameters=('old_name',)
+ )
+
+ # 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')
+ )
+
+ # 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()
self.tick_count += 1
@@ -137,28 +527,12 @@ 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
+ 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:
@@ -170,16 +544,16 @@ 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
+ _base_dir, '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:
- 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 +578,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 +591,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,24 +600,34 @@ 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.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):
@@ -256,7 +638,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 +652,45 @@ 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 get_encryption_lib(self):
+ 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'
+
+ 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 or set encrypt_location in config.')
+ self.logger.info('Platform: '+ _platform + ' Encrypt.so directory: '+ path)
+ sys.exit(1)
+ else:
+ self.logger.info('Found '+ file_name +'! Platform: ' + _platform + ' Encrypt.so directory: ' + path)
+
+ return full_path
def _setup_api(self):
# instantiate pgoapi
@@ -279,12 +700,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.api.activate_signature(self.get_encryption_lib())
+ self.logger.info('')
self.update_inventory()
# send empty map_cells and then our position
self.update_web_location()
@@ -301,7 +721,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 +741,58 @@ 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]) +
+ ' | MasterBalls: ' + str(items_stock[4]))
- 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]) +
+ ' | MaxPotion: ' + str(items_stock[104]))
- 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)
@@ -381,6 +803,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(
@@ -396,7 +820,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)
@@ -450,6 +874,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 +888,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...')
- with open('data/last-location-%s.json' %
- self.config.username) as f:
+ self.event_manager.emit(
+ 'load_cached_location',
+ sender=self,
+ level='debug',
+ formatted='Loading cached location...'
+ )
+ 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'],
@@ -487,22 +946,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'
+ 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(
- '[x] Last in-game location was set as: {}'.format(
- self.position
- )
- )
- logger.log('')
has_position = True
except Exception:
@@ -510,9 +975,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 +991,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.'
)
@@ -544,7 +1011,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()
@@ -578,22 +1053,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
@@ -606,8 +1081,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(
@@ -632,3 +1107,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/api_wrapper.py b/pokemongo_bot/api_wrapper.py
index a3f70aed1f..7224b0b4e4 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):
@@ -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
@@ -42,6 +41,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
@@ -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,18 +117,18 @@ 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')
+ 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/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
new file mode 100644
index 0000000000..1b610d31aa
--- /dev/null
+++ b/pokemongo_bot/base_task.py
@@ -0,0 +1,32 @@
+import logging
+
+
+class BaseTask(object):
+ TASK_API_VERSION = 1
+
+ def __init__(self, bot, config):
+ self.bot = bot
+ self.config = config
+ self._validate_work_exists()
+ self.logger = logging.getLogger(type(self).__name__)
+ self.enabled = config.get('enabled', True)
+ 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/__init__.py b/pokemongo_bot/cell_workers/__init__.py
index bc6638d1fd..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
@@ -15,7 +16,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_live_stats import UpdateLiveStats
diff --git a/pokemongo_bot/cell_workers/base_task.py b/pokemongo_bot/cell_workers/base_task.py
deleted file mode 100644
index 4d73a68443..0000000000
--- a/pokemongo_bot/cell_workers/base_task.py
+++ /dev/null
@@ -1,14 +0,0 @@
-class BaseTask(object):
- def __init__(self, bot, config):
- self.bot = bot
- self.config = config
- self._validate_work_exists()
- 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 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..afddeb53d5 100644
--- a/pokemongo_bot/cell_workers/catch_lured_pokemon.py
+++ b/pokemongo_bot/cell_workers/catch_lured_pokemon.py
@@ -1,38 +1,69 @@
-from pokemongo_bot import logger
-from pokemongo_bot.cell_workers.utils import fort_details
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+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
-from pokemongo_bot.cell_workers.base_task import BaseTask
+
class CatchLuredPokemon(BaseTask):
+ SUPPORTED_TASK_API_VERSION = 1
+
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 []
+
+ 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)
- 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)
+ for fort in forts_in_range:
+ details = fort_details(self.bot, fort_id=fort['id'],
+ latitude=fort['latitude'],
+ longitude=fort['longitude'])
+ fort_name = details.get('name', 'Unknown')
+ encounter_id = fort['lure_info']['encounter_id']
- if encounter_id:
- logger.log('Lured pokemon at fort {}'.format(fort_name))
- return {
+ result = {
'encounter_id': encounter_id,
'fort_id': fort['id'],
+ 'fort_name': u"{}".format(fort_name),
'latitude': fort['latitude'],
'longitude': fort['longitude']
}
+ pokemon_to_catch.append(result)
- return False
+ self.emit_event(
+ 'lured_pokemon_found',
+ formatted='Lured pokemon at fort {fort_name} ({fort_id})',
+ data=result
+ )
+ 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 7fe9df6cb4..0203459c28 100644
--- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py
+++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py
@@ -1,35 +1,69 @@
import json
+import os
-from pokemongo_bot import logger
-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
+from pokemongo_bot.worker_result import WorkerResult
+from pokemongo_bot.base_dir import _base_dir
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:
- logger.log('Something rustles nearby!')
+ 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(
key=
- 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
+ lambda x: distance(self.bot.position[0], self.bot.position[1], x['latitude'], x['longitude'])
+ )
+ 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)
+ 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))
+ 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/collect_level_up_reward.py b/pokemongo_bot/cell_workers/collect_level_up_reward.py
index 912a97d197..950f450660 100644
--- a/pokemongo_bot/cell_workers/collect_level_up_reward.py
+++ b/pokemongo_bot/cell_workers/collect_level_up_reward.py
@@ -1,8 +1,9 @@
-from pokemongo_bot import logger
-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
@@ -19,7 +20,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 +40,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..13299f0ed8 100644
--- a/pokemongo_bot/cell_workers/evolve_pokemon.py
+++ b/pokemongo_bot/cell_workers/evolve_pokemon.py
@@ -1,152 +1,155 @@
-from pokemongo_bot import logger
+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.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
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)
+ 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()
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():
return
- response_dict = self.api.get_inventory()
- inventory_items = response_dict.get('responses', {}).get('GET_INVENTORY', {}).get('inventory_delta', {}).get(
- 'inventory_items', {})
+ cache = set()
- evolve_list = self._sort_and_filter(inventory_items)
+ 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())]
- 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))
-
- 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
+ # 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:
- 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
+ self.emit_event(
+ 'used_lucky_egg',
+ formatted='Used lucky egg ({amount_left} left).',
+ data={
+ 'amount_left': lucky_egg_count - 1
+ }
+ )
else:
- logger.log('Failed to use lucky egg!', 'red')
- return False
- else:
- # Skipping evolve so they aren't wasted
- logger.log('No lucky eggs... skipping evolve!', 'yellow')
+ self.emit_event(
+ 'lucky_egg_error',
+ level='error',
+ formatted='Failed to use lucky egg!'
+ )
+ 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
- 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
+ # 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
- return candies
+ # Otherwise try evolving
+ return True
- 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:
+ 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
- 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
+ if not pokemon.has_seen_next_evolution():
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 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
+
- 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:
- logger.log('Successfully evolved {} with {} CP and {} IV!'.format(pokemon_name, pokemon_cp, pokemon_iv))
- candy_list[pokemon["candies_family"]] -= pokemon["candies_amount"]
+ 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,
+ 'ncp': '?',
+ 'dps': '?',
+ 'xp': '?'
+ }
+ )
+ 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:
# cache pokemons we can't evolve. Less server calls
- cache[pokemon_name] = 1
+ cache.add(pokemon.name)
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
index 9c845404bd..8448fcf742 100644
--- a/pokemongo_bot/cell_workers/follow_cluster.py
+++ b/pokemongo_bot/cell_workers/follow_cluster.py
@@ -1,16 +1,15 @@
-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.base_task import BaseTask
+class FollowCluster(BaseTask):
+ SUPPORTED_TASK_API_VERSION = 1
-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 +38,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 +70,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..1532695bd8 100644
--- a/pokemongo_bot/cell_workers/follow_path.py
+++ b/pokemongo_bot/cell_workers/follow_path.py
@@ -3,8 +3,7 @@
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.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
@@ -12,14 +11,22 @@
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:
@@ -34,11 +41,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
@@ -60,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/follow_spiral.py b/pokemongo_bot/cell_workers/follow_spiral.py
index bda348490a..f175369e45 100644
--- a/pokemongo_bot/cell_workers/follow_spiral.py
+++ b/pokemongo_bot/cell_workers/follow_spiral.py
@@ -3,12 +3,13 @@
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
+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)
@@ -84,10 +85,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..cf88eeac83 100644
--- a/pokemongo_bot/cell_workers/handle_soft_ban.py
+++ b/pokemongo_bot/cell_workers/handle_soft_ban.py
@@ -2,15 +2,16 @@
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.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
@@ -18,9 +19,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],
@@ -30,29 +29,46 @@ 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']]
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):
+ 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.emit_event(
+ 'spun_fort',
+ level='debug',
+ formatted="Spun fort {fort_id}",
+ data={
+ 'fort_id': fort_id,
+ 'latitude': latitude,
+ 'longitude': 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..805129435b 100644
--- a/pokemongo_bot/cell_workers/incubate_eggs.py
+++ b/pokemongo_bot/cell_workers/incubate_eggs.py
@@ -1,9 +1,10 @@
-from pokemongo_bot import logger
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):
@@ -31,7 +32,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
@@ -42,11 +49,20 @@ 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
- 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 +70,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 +180,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..4d0ff4896f 100644
--- a/pokemongo_bot/cell_workers/move_to_fort.py
+++ b/pokemongo_bot/cell_workers/move_to_fort.py
@@ -1,23 +1,31 @@
-from pokemongo_bot import logger
+# -*- 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
-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
- 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:
- 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')
- return has_space_for_loot or self.bot.softban
+ 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 has_space_for_loot or self.ignore_item_count or self.bot.softban
def is_attracted(self):
return (self.lure_distance > 0)
@@ -35,7 +43,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 +55,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 +84,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/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py
index bce39e0143..efd058ca96 100644
--- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py
+++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py
@@ -1,26 +1,92 @@
# -*- 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.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
-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
+# 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):
self.last_map_update = 0
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)
+ 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)
@@ -28,15 +94,17 @@ 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:
- 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 = []
@@ -46,7 +114,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)
@@ -98,14 +166,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],
@@ -116,36 +187,44 @@ 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):
- 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)
@@ -174,24 +253,119 @@ 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():
+ 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, 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())
- 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/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py
index 2c61d87a9f..493d2656ff 100644
--- a/pokemongo_bot/cell_workers/nickname_pokemon.py
+++ b/pokemongo_bot/cell_workers/nickname_pokemon.py
@@ -1,87 +1,427 @@
-from pokemongo_bot import logger
+from pokemongo_bot.base_task import BaseTask
from pokemongo_bot.human_behaviour import sleep
-from pokemongo_bot.cell_workers.base_task import BaseTask
+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):
- 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:
- 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']
- 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'
+
+ # 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:
- 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(old_nickname, bad_key)
+ )
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:
- logger.log("Attempt to nickname received bad response from server.",log_color)
- if self.bot.config.debug:
- logger.log(response,log_color)
+ self.emit_event(
+ 'api_error',
+ formatted='Attempt to nickname received bad response from server.'
+ )
return
- 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'
- pokemon['nickname'] = new_name
- logger.log(output,log_color)
\ No newline at end of file
+
+ # Nickname unset
+ if result == 0:
+ self.emit_event(
+ 'unset_pokemon_nickname',
+ 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': old_nickname, 'current_name': new_nickname}
+ )
+ pokemon.update_nickname(new_nickname)
+ elif result == 2:
+ self.emit_event(
+ 'pokemon_nickname_invalid',
+ formatted="Nickname {nickname} is invalid",
+ 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/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py
index 3f610e7558..d30718eaea 100644
--- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py
+++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py
@@ -1,13 +1,33 @@
# -*- coding: utf-8 -*-
import time
-from pokemongo_bot import logger
-from pokemongo_bot.human_behaviour import (normalized_reticle_size, sleep,
- spin_modifier)
+from random import random
+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
-class PokemonCatchWorker(object):
- BAG_FULL = 'bag_full'
- NO_POKEBALLS = 'no_pokeballs'
+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 PokemonCatchWorker(BaseTask):
def __init__(self, pokemon, bot):
self.pokemon = pokemon
@@ -16,352 +36,73 @@ 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 = ''
+ ############################################################################
+ # 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']
- logger.log('A Wild {} appeared! [CP {}] [Potential {}]'.format(
- pokemon_name, cp, pokemon_potential), 'yellow')
-
- logger.log('IV [Attack/Defense/Stamina] = [{}]'.format(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')
- 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)
- 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))
- # 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
- logger.log('Threw a berry! Catch Rate with normal Pokeball has increased to {}%'.format(success_percentage))
- else:
- if response_dict['status_code'] is 1:
- logger.log('Fail to use berry. Seem like you are softbanned.', 'red')
- else:
- logger.log('Fail to use berry. Status Code: {}'.format(response_dict['status_code']),'red')
-
- #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)
- 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))
- 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
- logger.log('Threw a berry! Catch Rate with normal Pokeball has increased to {}%'.format(success_percentage))
- else:
- if response_dict['status_code'] is 1:
- logger.log('Fail to use berry. Seem like you are softbanned.', 'red')
- else:
- logger.log('Fail to use berry. Status Code: {}'.format(response_dict['status_code']),'red')
-
- 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)
- 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))
- 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
- logger.log('Threw a berry! Catch Rate with normal Pokeball has increased to {}%'.format(success_percentage))
- else:
- if response_dict['status_code'] is 1:
- logger.log('Fail to use berry. Seem like you are softbanned.', 'red')
- else:
- logger.log('Fail to use berry. Status Code: {}'.format(response_dict['status_code']),'red')
-
- # 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)
- logger.log('Using {} (chance: {}%)... ({} left!)'.format(
- self.item_list[str(pokeball)],
- success_percentage,
- items_stock[pokeball]
- ))
-
- id_list1 = self.count_pokemon_inventory()
-
- reticle_size_parameter = normalized_reticle_size(self.config.catch_randomize_reticle_factor)
- spin_modifier_parameter = spin_modifier(self.config.catch_randomize_spin_factor)
-
- 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:
- logger.log(
- '[-] Attempted to capture {} - failed.. trying again!'.format(pokemon_name),
- 'red')
- sleep(2)
- continue
- if status is 3:
- logger.log(
- 'Oh no! {} vanished! :('.format(pokemon_name), 'red')
- 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.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:
- logger.log(
- '{} has been evolved!'.format(pokemon_name), 'green')
- else:
- logger.log(
- 'Failed to evolve {}!'.format(pokemon_name))
- break
- time.sleep(5)
+ return WorkerResult.ERROR
- 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):
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
- }
-
- # 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())
+ return WorkerResult.ERROR
+
+ # get pokemon data
+ pokemon_data = response['wild_pokemon']['pokemon_data'] if 'wild_pokemon' in response else response['pokemon_data']
+ pokemon = Pokemon(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,
+ 'encounter_id': self.pokemon['encounter_id'],
+ 'latitude': self.pokemon['latitude'],
+ 'longitude': self.pokemon['longitude'],
+ 'pokemon_id': pokemon.pokemon_id
+ }
+ )
+
+ # 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']
@@ -393,30 +134,322 @@ 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 _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.inventory.get(current_ball).name,
+ 'berry_name': self.inventory.get(berry_id).name,
+ '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.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])
+ }
+ )
+
+ # 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
+ """
+
+ :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
+
+ 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
+
+ # use `min_ultraball_to_keep` from config if is not None
+ 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
+
+ while True:
+
+ # find lowest available ball
+ current_ball = ITEM_POKEBALL
+ while ball_count[current_ball] == 0 and current_ball < maximum_ball:
+ current_ball += 1
+ if ball_count[current_ball] == 0:
+ 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 ball_count[ITEM_ULTRABALL] > min_ultraball_to_keep:
+ maximum_ball = ITEM_ULTRABALL
+ continue
+ else:
+ break
+
+ # check future ball count
+ num_next_balls = 0
+ next_ball = current_ball
+ while next_ball < maximum_ball:
+ next_ball += 1
+ 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
+
+ # 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:
+ 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 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:
+ 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,
+ '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
+ 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.inventory.get(current_ball).name,
+ 'success_percentage': self._pct(catch_rate_by_ball[current_ball]),
+ 'count_left': ball_count[current_ball]
+ }
+ )
+
+ response_dict = self.api.catch_pokemon(
+ encounter_id=encounter_id,
+ pokeball=current_ball,
+ normalized_reticle_size=throw_parameters['normalized_reticle_size'],
+ spawn_point_id=self.spawn_point_guid,
+ hit_pokemon=1,
+ spin_modifier=throw_parameters['spin_modifier'],
+ normalized_hit_position=throw_parameters['normalized_hit_position']
+ )
+
+ 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,
+ 'encounter_id': self.pokemon['encounter_id'],
+ 'latitude': self.pokemon['latitude'],
+ 'longitude': self.pokemon['longitude'],
+ 'pokemon_id': pokemon.pokemon_id
+ }
+ )
+ if self._pct(catch_rate_by_ball[current_ball]) == 100:
+ self.bot.softban = True
+
+ # 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]',
+ 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']),
+ 'encounter_id': self.pokemon['encounter_id'],
+ 'latitude': self.pokemon['latitude'],
+ 'longitude': self.pokemon['longitude'],
+ 'pokemon_id': pokemon.pokemon_id
+ }
+ )
+
+ # We could refresh here too, but adding 3 saves a inventory request
+ candy = inventory.candies(True).get(pokemon.pokemon_id)
+ self.emit_event(
+ 'gained_candy',
+ formatted='You now have {quantity} {type} candy!',
+ data = {
+ 'quantity': candy.quantity,
+ 'type': candy.type,
+ },
+ )
+
+ 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/cell_workers/pokemon_optimizer.py b/pokemongo_bot/cell_workers/pokemon_optimizer.py
new file mode 100644
index 0000000000..81f78ddf33
--- /dev/null
+++ b/pokemongo_bot/cell_workers/pokemon_optimizer.py
@@ -0,0 +1,328 @@
+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.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)
+ 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):
+ 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:
+ 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)
+ 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)
+
+ 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)
+
+ 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
+
+ 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]
+ else:
+ evo_crap = []
+ transfer = crap
+
+ return (transfer, can_evolve_best, evo_crap)
+
+ def apply_optimization(self, transfer, evo):
+ for pokemon in transfer:
+ self.transfer_pokemon(pokemon)
+
+ 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 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))
+
+ for pokemon in evo:
+ self.evolve_pokemon(pokemon)
+
+ def transfer_pokemon(self, pokemon):
+ if self.config_transfer and (not self.bot.config.test):
+ response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon.id)
+ else:
+ 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}] [NCP {ncp}] [DPS {dps}]",
+ data={"pokemon": pokemon.name,
+ "iv": pokemon.iv,
+ "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)
+
+ 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 lucky_egg.count == 0:
+ return False
+
+ response_dict = self.bot.use_lucky_egg()
+
+ 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:
+ lucky_egg.remove(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 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}}}
+
+ if not response_dict:
+ 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", {})
+
+ 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 and (not self.bot.config.test):
+ inventory.candies().get(pokemon.pokemon_id).consume(pokemon.evolution_cost - candy)
+ inventory.pokemons().remove(pokemon.id)
+
+ new_pokemon = inventory.Pokemon(evolution)
+ inventory.pokemons().add(new_pokemon)
+
+ sleep(20)
+
+ return True
diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py
index 022a711f1a..e48dff3cd8 100644
--- a/pokemongo_bot/cell_workers/recycle_items.py
+++ b/pokemongo_bot/cell_workers/recycle_items.py
@@ -1,52 +1,131 @@
import json
import os
-from pokemongo_bot import logger
-from pokemongo_bot.cell_workers.base_task import BaseTask
+
+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
+
+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.item_filter = self.config.get('item_filter', {})
+ self.items_filter = self.config.get('item_filter', {})
+ self.min_empty_space = self.config.get('min_empty_space', None)
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():
+ """
+ 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.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))
+ 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):
- 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
- 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')
- else:
- logger.log("-- Failed to discard " + item_name, 'red')
-
- 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):
+ 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
+
+ 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/cell_workers/sleep_schedule.py b/pokemongo_bot/cell_workers/sleep_schedule.py
index 221867f0e6..5a7d617e23 100644
--- a/pokemongo_bot/cell_workers/sleep_schedule.py
+++ b/pokemongo_bot/cell_workers/sleep_schedule.py
@@ -1,8 +1,7 @@
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
+from pokemongo_bot.base_task import BaseTask
class SleepSchedule(BaseTask):
@@ -28,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
@@ -63,7 +63,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 +93,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..eee88525c5 100644
--- a/pokemongo_bot/cell_workers/spin_fort.py
+++ b/pokemongo_bot/cell_workers/spin_fort.py
@@ -1,37 +1,54 @@
# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+import json
+import os
import time
from pgoapi.utilities import f2i
+from pokemongo_bot import inventory
-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
-from pokemongo_bot.cell_workers.base_task import BaseTask
+from pokemongo_bot.base_task import BaseTask
+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
+
+ 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():
- 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')
- return False
- return True
+ 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 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']
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'],
@@ -40,100 +57,133 @@ 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
- 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:
- 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'
- )
- else:
- logger.log("[#] Nothing found.", 'yellow')
+ experience_awarded = spin_details.get('experience_awarded', 0)
+
+ items_awarded = self.get_items_awarded_from_fort_spinned(response_dict)
+
+ 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': items_awarded
+ }
+ )
+ 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})
- 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")
- elif spin_result == 3:
+ 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 == SPIN_REQUEST_RESULT_IN_COOLDOWN_PERIOD:
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)))
- elif spin_result == 4:
- logger.log("Inventory is full", 'red')
+ 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 == SPIN_REQUEST_RESULT_INVENTORY_FULL:
+ if not self.ignore_item_count:
+ 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
+ return WorkerResult.ERROR
sleep(2)
- return 0
- def get_fort_in_range(self):
- forts = self.bot.get_forts(order_by_distance=True)
+ if len(forts) > 1:
+ return WorkerResult.RUNNING
- forts = filter(lambda x: x["id"] not in self.bot.fort_timeouts, forts)
+ return WorkerResult.SUCCESS
- if len(forts) == 0:
- return None
+ def get_forts_in_range(self):
+ forts = self.bot.get_forts(order_by_distance=True)
- fort = forts[0]
+ 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)
- 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']
- )
+ ) <= 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
- if distance_to_fort <= Constants.MAX_DISTANCE_FORT_IS_REACHABLE:
- return fort
+ # 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'])
- return None
diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py
index 374ffa6a87..f9a6da6fc4 100644
--- a/pokemongo_bot/cell_workers/transfer_pokemon.py
+++ b/pokemongo_bot/cell_workers/transfer_pokemon.py
@@ -1,135 +1,87 @@
import json
+import os
-from pokemongo_bot import logger
+from pokemongo_bot import inventory
from pokemongo_bot.human_behaviour import action_delay
-from pokemongo_bot.cell_workers.base_task import BaseTask
+from pokemongo_bot.base_task import BaseTask
+from pokemongo_bot.inventory import Pokemons, Pokemon
+
class TransferPokemon(BaseTask):
+ SUPPORTED_TASK_API_VERSION = 1
+
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:
- 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:
- 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:
+ # 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
- 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')
@@ -154,11 +106,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 = {
@@ -167,24 +119,51 @@ 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': pokemon.cp,
+ 'iv': pokemon.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)
+ def release_pokemon(self, pokemon):
+ """
+
+ :type pokemon: 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)
+ inventory.pokemons().remove(pokemon.id)
+ self.bot.metrics.released_pokemon()
+ self.emit_event(
+ 'pokemon_release',
+ formatted='Exchanged {pokemon} [CP {cp}] [IV {iv}] for candy.',
+ data={
+ 'pokemon': pokemon.name,
+ 'cp': pokemon.cp,
+ '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)
def _get_release_config_for(self, pokemon):
@@ -216,7 +195,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/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_live_stats.py
similarity index 73%
rename from pokemongo_bot/cell_workers/update_title_stats.py
rename to pokemongo_bot/cell_workers/update_live_stats.py
index d255834870..e51253edc5 100644
--- a/pokemongo_bot/cell_workers/update_title_stats.py
+++ b/pokemongo_bot/cell_workers/update_live_stats.py
@@ -2,28 +2,40 @@
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
-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",
+ "type": "UpdateLiveStats",
"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"],
+ "terminal_log": true,
+ "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).
- uptime : The bot uptime.
- km_walked : The kilometers walked since the bot started.
- level : The current character's level.
@@ -35,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.
@@ -43,16 +56,8 @@ 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.
"""
-
- DEFAULT_MIN_INTERVAL = 10
- DEFAULT_DISPLAYED_STATS = []
+ SUPPORTED_TASK_API_VERSION = 1
def __init__(self, bot, config):
"""
@@ -62,43 +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 = self.DEFAULT_MIN_INTERVAL
- self.displayed_stats = self.DEFAULT_DISPLAYED_STATS
- self._process_config()
+ self.min_interval = int(self.config.get('min_interval', 120))
+ self.displayed_stats = self.config.get('stats', [])
+ 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('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
- self._update_title(title, _platform)
+
+ if self.terminal_title:
+ self._update_title(line, _platform)
+
+ if self.terminal_log:
+ 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 _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="{stats}",
+ data={
+ 'stats': stats
+ }
+ )
+ 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.
@@ -107,26 +147,20 @@ 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 == "darwin" or platform == "cygwin":
+
+ 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)
+ 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 _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):
+ 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.
@@ -143,6 +177,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))
@@ -156,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()
@@ -170,6 +208,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),
@@ -183,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),
@@ -211,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/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..cedd0e53dc
--- /dev/null
+++ b/pokemongo_bot/event_handlers/colored_logging_handler.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+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',
+
+ # 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 = {
+ '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 handle_event(self, event, sender, level, formatted_msg, data):
+ logger = logging.getLogger(type(sender).__name__)
+
+ 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':
+ color = self.COLOR_CODE['red']
+ formatted_msg = '{}{}{}'.format(color, formatted_msg, self.COLOR_CODE['reset'])
+
+ 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/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..3d759bf666 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 []
+ self._handlers = list(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..55a726049d 100644
--- a/pokemongo_bot/health_record/bot_event.py
+++ b/pokemongo_bot/health_record/bot_event.py
@@ -1,41 +1,82 @@
# -*- coding: utf-8 -*-
from time import sleep
-from UniversalAnalytics import Tracker
-
-from pokemongo_bot import logger
-
+import logging
+from raven import Client
+import raven
+import os
+import uuid
+import requests
+import time
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.log(
- '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 information:')
+ 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.log('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
+ 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.bot.config.health_record:
- self.tracker.send('pageview', '/loggedin', title='succ')
+ if self.config.health_record:
+ self.last_heartbeat = time.time()
+ self.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:
+ self.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:
+ self.track_url('/relogin')
def logout(self):
- if self.bot.config.health_record:
- self.tracker.send('pageview', '/logout', title='logout')
+ if self.config.health_record:
+ 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
+ self.track_url('/heartbeat')
+
+ 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()
+ except requests.exceptions.HTTPError:
+ pass
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)
diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py
new file mode 100644
index 0000000000..ccac6f43be
--- /dev/null
+++ b/pokemongo_bot/inventory.py
@@ -0,0 +1,1147 @@
+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/
+'''
+
+
+#
+# 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):
+ 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__()
+
+ 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, object_id):
+ return self._data.get(object_id)
+
+ def all(self):
+ return list(self._data.values())
+
+
+#
+# Inventory Components
+
+class Candies(_BaseInventoryComponent):
+ TYPE = 'candy'
+ ID_FIELD = 'family_id'
+
+ @classmethod
+ def family_id_for(cls, 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 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'
+ ID_FIELD = 'item_id'
+ STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'items.json')
+
+ 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)]
+
+ @classmethod
+ def get_space_used(cls):
+ """
+ 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
+
+ @classmethod
+ def get_space_left(cls):
+ """
+ Compute 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()
+ 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):
+ TYPE = 'pokemon_data'
+ ID_FIELD = 'id'
+ STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'pokemon.json')
+
+ @classmethod
+ def process_static_data(cls, data):
+ data = [PokemonInfo(d) for d in data]
+
+ # process evolution info
+ for p in data:
+ next_all = p.next_evolutions_all
+ if len(next_all) <= 0:
+ continue
+
+ # 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]
+
+ # 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
+
+ return data
+
+ @classmethod
+ def data_for(cls, pokemon_id):
+ # type: (int) -> PokemonInfo
+ 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):
+ return cls.data_for(pokemon_id).first_evolution_id
+
+ @classmethod
+ def prev_evolution_id_for(cls, pokemon_id):
+ return cls.data_for(pokemon_id).prev_evolution_id
+
+ @classmethod
+ def next_evolution_ids_for(cls, pokemon_id):
+ return cls.data_for(pokemon_id).next_evolution_ids
+
+ @classmethod
+ def last_evolution_ids_for(cls, pokemon_id):
+ return cls.data_for(pokemon_id).last_evolution_ids
+
+ @classmethod
+ def has_next_evolution(cls, pokemon_id):
+ return cls.data_for(pokemon_id).has_next_evolution
+
+ @classmethod
+ def evolution_cost_for(cls, pokemon_id):
+ return cls.data_for(pokemon_id).evolution_cost
+
+ 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)]
+
+ 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
+
+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
+ 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)
+ assert cls.MAX_CPM > .0
+
+ @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
+
+ 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)
+ 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: (Union[string, Type]) -> List[Attack]
+ """
+ :return: Attacks sorted by DPS in descending order
+ """
+ return cls.BY_TYPE[str(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 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)
+ 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
+
+ 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
+ # Unique ID for this particular Pokemon
+ 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']
+ # 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 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)
+
+ # 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
+
+ self.fast_attack = FastAttacks.data_for(data['move_1'])
+ self.charged_attack = ChargedAttacks.data_for(data['move_2']) # type: ChargedAttack
+
+ # Individial 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(
+ 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.static.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 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 self.static.has_next_evolution
+
+ def has_seen_next_evolution(self):
+ for pokemon_id in self.next_evolution_ids:
+ if pokedex().captured(pokemon_id):
+ return True
+ return False
+
+ @property
+ def family_id(self):
+ return self.static.family_id
+
+ @property
+ def first_evolution_id(self):
+ return self.static.first_evolution_id
+
+ @property
+ def prev_evolution_id(self):
+ return self.static.prev_evolution_id
+
+ @property
+ def next_evolution_ids(self):
+ return self.static.next_evolution_ids
+
+ @property
+ def last_evolution_ids(self):
+ return self.static.last_evolution_ids
+
+ @property
+ def candy_quantity(self):
+ return candies().get(self.pokemon_id).quantity
+
+ @property
+ def evolution_cost(self):
+ return self.static.evolution_cost
+
+ @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)
+ 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.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,
+ 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.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.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 = Types.get(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 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
+
+ @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[Type], 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 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
+
+ # 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):
+ def __init__(self, bot):
+ self.bot = bot
+ self.pokedex = Pokedex()
+ self.candy = Candies()
+ 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
+ # 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)
+
+ 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']
+
+
+#
+# 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 # type: Inventory
+
+
+def _calc_cp(base_attack, base_defense, base_stamina,
+ iv_attack=15, iv_defense=15, iv_stamina=15,
+ cp_multiplier=.0):
+ """
+ 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
+ """
+ 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)
+
+
+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
+
+
+def candies(refresh=False):
+ if refresh:
+ refresh_inventory()
+ return _inventory.candy
+
+
+def pokemons(refresh=False):
+ if refresh:
+ refresh_inventory()
+ return _inventory.pokemons
+
+
+def items():
+ """
+ Access to the cached item inventory
+ :return: Instance of the cached item inventory
+ :rtype: Items
+ """
+ return _inventory.items
+
+
+def types_data():
+ return Types
+
+
+def levels_to_cpm():
+ return LevelToCPm
+
+
+def fast_attacks():
+ return FastAttacks
+
+
+def charged_attacks():
+ return ChargedAttacks
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/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/pokemongo_bot/plugin_loader.py b/pokemongo_bot/plugin_loader.py
new file mode 100644
index 0000000000..f7e12a85a7
--- /dev/null
+++ b/pokemongo_bot/plugin_loader.py
@@ -0,0 +1,148 @@
+import os
+import sys
+import importlib
+import re
+import requests
+import zipfile
+import shutil
+
+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_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)
+ sys.path.append(correct_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/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
+ }
+ )
diff --git a/pokemongo_bot/socketio_server/app.py b/pokemongo_bot/socketio_server/app.py
index 4aac36c202..a970a30479 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['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['event']
+ account = env['account']
+ event_name = "{}:{}".format(event, account)
+ sio.emit(event_name, data=env)
diff --git a/pokemongo_bot/step_walker.py b/pokemongo_bot/step_walker.py
index 263699095d..97a4f14b47 100644
--- a/pokemongo_bot/step_walker.py
+++ b/pokemongo_bot/step_walker.py
@@ -39,6 +39,18 @@ 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
totalDLat = (self.destLat - self.initLat)
@@ -54,6 +66,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/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/test/plugin_loader_test.py b/pokemongo_bot/test/plugin_loader_test.py
new file mode 100644
index 0000000000..ed285ede67
--- /dev/null
+++ b/pokemongo_bot/test/plugin_loader_test.py
@@ -0,0 +1,148 @@
+import imp
+import sys
+import pkgutil
+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
+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_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_plugin(package_path)
+ loaded_class = self.plugin_loader.get_class('plugin_fixture_test.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/__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 0000000000..78828798c3
Binary files /dev/null and b/pokemongo_bot/test/resources/plugin_fixture_test.zip differ
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 0000000000..a692ac3f08
Binary files /dev/null and b/pokemongo_bot/test/resources/test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip differ
diff --git a/pokemongo_bot/tree_config_builder.py b/pokemongo_bot/tree_config_builder.py
index bba63ad880..57dc9da33c 100644
--- a/pokemongo_bot/tree_config_builder.py
+++ b/pokemongo_bot/tree_config_builder.py
@@ -1,12 +1,18 @@
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
self.tasks_raw = tasks_raw
+ self.plugin_loader = PluginLoader()
def _get_worker_by_name(self, name):
try:
@@ -16,6 +22,9 @@ def _get_worker_by_name(self, name):
return worker
+ def _is_plugin_task(self, name):
+ return '.' in name
+
def build(self):
workers = []
@@ -28,9 +37,32 @@ 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)
+
+ 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)
+ if instance.enabled:
+ workers.append(instance)
return workers
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..cd5bd5af96 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
@@ -38,11 +42,17 @@ 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',
{
- 'response': player_info,
- 'command': 'get_player_info'
+ 'result': {'inventory': inventory, 'player': 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
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 08f9407c81..76b1d15a7b 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@a2755eb42dfe49e359798d2f4defefc97fb8163d#egg=pgoapi
geopy==1.11.0
protobuf==3.0.0b4
requests==2.10.0
@@ -17,7 +17,8 @@ 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
+demjson==2.2.4
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 ec95acb3e3..fa6d8ffab1 100755
--- a/run.sh
+++ b/run.sh
@@ -1,18 +1,21 @@
#!/usr/bin/env bash
-
-# Starts PokemonGo-Bot
-config=""
-
+pokebotpath=$(cd "$(dirname "$0")"; 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"
fi
-
-python pokecli.py --config ${config}
+cd $pokebotpath
+source bin/activate
+if [ ! -f "$filename" ]; then
+echo "There's no "$filename" file. Please use ./setup.sh -c to creat one."
+fi
+while true
+do
+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
+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..9535bc7d10
--- /dev/null
+++ b/setup.sh
@@ -0,0 +1,145 @@
+#!/usr/bin/env bash
+#encoding=utf8
+pokebotpath=$(cd "$(dirname "$0")"; pwd)
+backuppath=$pokebotpath"/backup"
+
+function Pokebotupdate () {
+cd $pokebotpath
+git pull
+git submodule update --init --recursive
+git submodule foreach git pull origin master
+virtualenv .
+source bin/activate
+pip install -r requirements.txt
+}
+
+function Pokebotencrypt () {
+echo "Start to make encrypt.so"
+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
+mv libencrypt.so $pokebotpath/encrypt.so
+cd ../..
+rm -rf pgoencrypt.tar.gz
+rm -rf pgoencrypt
+}
+
+function Pokebotconfig () {
+cd $pokebotpath
+read -p "enter 1 for google or 2 for ptc
+" auth
+read -p "Input username
+" username
+read -p "Input password
+" -s password
+read -p "
+Input location
+" location
+read -p "Input gmapkey
+" gmapkey
+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
+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 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
+sudo pip install virtualenv
+Pokebotupdate
+Pokebotencrypt
+echo "Install complete. Starting to generate config.json."
+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 -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"
+;;
+--config|-c)
+Pokebotconfig
+;;
+--help|-h)
+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."
+else
+./run.sh ./configs/"$filename"
+fi
+;;
+*)
+Pokebothelp
+;;
+esac
+exit 0
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/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/inventory_test.py b/tests/inventory_test.py
new file mode 100644
index 0000000000..4070344ca4
--- /dev/null
+++ b/tests/inventory_test.py
@@ -0,0 +1,215 @@
+import unittest
+
+from pokemongo_bot.inventory import *
+
+
+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
+
+ obj = Pokemons
+ self.assertEqual(len(obj.STATIC_DATA), 151)
+
+ 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)
+ 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 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 = 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)
+ assert 12 <= candies_cost <= 400
+ self.assertEqual(reqs["Amount"], candies_cost)
+
+ evolutions = pokemon._data["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)._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)
+
+ # Only Eevee has 3 next evolutions
+ self.assertEqual(len(next_evolution_ids),
+ 1 if pokemon_id != 133 else 3)
+
+ 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))
+
+ #
+ # 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.static.max_cp, 1921.34561459)
+ self.assertAlmostEqual(poke.cp_percent, 0.340368964)
+ 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)
+ 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.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, 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)
+ 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)
+ assert (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.assertIsInstance(attack.type, Type)
+ self.assertGreaterEqual(attack.damage, 0)
+ self.assertGreater(attack.duration, .0)
+ self.assertGreater(attack.energy, 0)
+ self.assertGreaterEqual(attack.dps, 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))
+ 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)
diff --git a/tests/tree_config_builder_test.py b/tests/tree_config_builder_test.py
index cee1080280..f8982cbfad 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,73 @@ 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_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()
+ plugin_loader.load_plugin(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_plugin(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_live_stats_test.py b/tests/update_live_stats_test.py
new file mode 100644
index 0000000000..dc5b140080
--- /dev/null
+++ b/tests/update_live_stats_test.py
@@ -0,0 +1,185 @@
+import unittest
+from sys import platform as _platform
+from datetime import datetime, timedelta
+from mock import call, patch, MagicMock
+from pokemongo_bot.cell_workers.update_live_stats import UpdateLiveStats
+from tests import FakeBot
+
+
+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', 'captures_per_hour'],
+ 'terminal_log': True,
+ 'terminal_title': False
+ }
+ player_stats = {
+ 'level': 25,
+ 'prev_level_xp': 1250000,
+ 'next_level_xp': 1400000,
+ 'experience': 1337500
+ }
+
+ def setUp(self):
+ self.bot = FakeBot()
+ self.bot._player = {'username': 'Username'}
+ self.bot.config.username = 'Login'
+ self.worker = UpdateLiveStats(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.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
+ 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_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_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)
+ self.worker.next_update = now
+
+ self.assertFalse(self.worker._should_display())
+
+ @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)
+ self.worker.next_update = now
+
+ self.assertTrue(self.worker._should_display())
+
+ @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
+ self.worker.next_update = now
+
+ self.assertTrue(self.worker._should_display())
+
+ @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._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_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_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_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_line_player_stats_none(self):
+ line = self.worker._get_stats_line(None)
+
+ self.assertEqual(line, '')
+
+ def test_get_stats_line_no_displayed_stats(self):
+ self.worker.displayed_stats = []
+ line = self.worker._get_stats_line(self.player_stats)
+
+ self.assertEqual(line, '')
+
+ def test_get_stats_line(self):
+ self.mock_metrics()
+
+ 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 | ' \
+ '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 | 75 pokemon/h'
+
+ self.assertEqual(line, expected)
diff --git a/tests/update_title_stats_test.py b/tests/update_title_stats_test.py
deleted file mode 100644
index 699d736b7a..0000000000
--- a/tests/update_title_stats_test.py
+++ /dev/null
@@ -1,131 +0,0 @@
-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/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()