From e8218f65a34119ee764cf6eece17df36337e5bec Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sat, 6 Aug 2016 13:42:52 -0700 Subject: [PATCH] Dev merge to master (#2627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [BUGFIX] Catch Pokemon while walking to fort (#979) Resolves: #821 * Added IGNORED_FILES list to pylint-recursive.py (#923) * Fixed a bug where the bot crashed if 'catch' or 'release' wasn't specified in config.json * Added install.sh and run.sh * Added IGNORED_FILES list to pylint-recursive.py * Removed --item_filter argument from pokecli Removed due to 'complexity' of new item_filter, therefore it should only be in config.json * Fix the teleporting on web ui (#1074) * Update README.md (#1063) The plural of pokemon is pokemon. * Broken connection fix (#1071) * Broken connection fix * Typos fix * Forgot to load itemfilter after deleting args input * Update README with Docker instructions (#759) * Add metrics logging and output on close (#1059) * Add metrics logging and output on close Output looks like the following: [17:10:07] Exiting PokemonGo Bot [17:10:07] [17:10:07] Ran for 0:00:20 [17:10:07] Total XP Earned: 210 Averaging: 36534.47/h [17:10:07] Travelled 0.01km [17:10:07] Visited 0 stops [17:10:07] Encountered 1 pokemon, 1 caught, 0 released, 0 evolved, 0 never seen before [17:10:07] Threw 1 pokeball [17:10:07] Earned 100 Stardust [17:10:07] [17:10:07] Highest CP Pokemon: Nidoran M [CP: 75] [IV: 1/10/5] Potential: 0.36 [17:10:07] Most Perfect Pokemon: Nidoran M [CP: 75] [IV: 1/10/5] Potential: 0.36 * Added Metrics class to collect end of run stats Tried to come up with a reasonable division of labour for how to gather the information. Open to feedback! * Revert logging changes Didn’t mean to affect this message any more. * Merge cells together to avoid staying in one cell too long (#1061) * Merge cells together to avoid staying in one cell too long This should help mitigate the issue where the bot travels to a stop that is farther than a nearby one because the nearer one is in another cell. I also release control back to the make loop after catching any pokemon. * PR Feedback fixes + Add concatenation of nearby cells rather than override. ~ Actually call the SeenFortWorker rather than just reference it. * Don't make work a property I seem to have made it one at some point, somehow… Go PyCharm! * Add check to ensure there are available gyms * Refactor EvolveAll and InitialTransfer workers (#941) * Refactor EvolveAll and InitialTransfer workers * Fixing Item import * Fixed 'Pokemon will now be caught from lures' (#1072) * Set evolve speed in config (#1090) * added evolve_speed * updated README.md to include evolve_speed * when filter set to 0, it will failed (#1101) * fixing item_list not found (#1120) * Fix Location caching doesn't work (#1031) (#1100) * Update _get_catch_config in pokemon_catch_worker (#1124) It should return the setting given by "any" in the catch_config file, instead of return {} for a "unspecified" pokemon. * unhappy api parameter name (#1137) * Making the metrics be printed correctly at the end of the run (#1136) * REVERT #1072 * FIX REVERTED #1072 * Moving logic for catching the visible pokemon out into a new worker (#1142) * Dump cells to enable custom front-end functionalities (#1145) * Dump cells to enable custom front-end functionalities Fixed merge issues for #1019 It now dumps the cell list as a json into data/cells-$username.json, so that more front-ends can use this information (I'm working on a Kivy-based one). * Updated ignore file skip new cellfiles * Adding a WorkerResult and the MoveToFortWorker only takes a single step towards a fort (#1146) * RecycleItemWorker implemented (runs on every tick) (#1156) * RecycleItemWorker implemented (runs on every tick) * moved RecycleItemWorker to a better place * recycle item worker logging improved * simplify if in item_inventory_count * removing extra space * Moving these flags into the workers. Make them run on each tick (#1159) * Removing duplicated release logic from catching pokemon (#1160) * Updated readme, contributors and gitignore file (#1161) * moved most of readme stuff to wiki in order for a clean readme table * added myself to contributors at last * removed old files from gitignore file * More items info at start (#1167) * Update __init__.py * Update pokemon_catch_worker.py * Update CONTRIBUTORS.md * removing modules that cant be imported (#1165) * removing modules that cant be imported * Updated pylint check It wont exit incorrect if no error exists * Fixing logspam for transferring pokemon (#1164) * Removing unnecessary walking from the move to fort worker (#1166) * Removed logging (#1171) Should not logg when logging is done once it finds something to release * Removing logging wrapping cleaning up the pokemon bag (#1172) * Revert "Removing duplicated release logic from catching pokemon" (#1188) * Hotfix/walk to pokestop and refactor to walker (#1193) * some nice refactoring to StepWalker to avoid repetition * removing sleep in navigator (bot should only sleep after walk and in workers) * fixing walk to pokestop * removing personal debug log from step walker * removing empty line * added random_lat_long_delta again to StepWalker (my bad) * completely removed walking progress bar (log trimming is required) * better log color when exchanging pokemon (red) * less sleep after spinning pokestop * spiral navigator shouldn't reuse step walker instance * Reduce number of inventory calls (#1231) There’s way too many API calls for the inventory, so I added a coached response that’s used until someone invalidates it (like when catching/throwing and spinning stops) * Add missing inventory fetch (#1233) To recycle an item with the correct number, we should force fetch from the server. * API update fixes (i2f etc) and lured pokemon catching (#1163) * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * API update fixes (i2f etc) and lured pokemon catching * Fix PogoAPI to a recent commit * Fix PogoAPI to a recent commit * Fix PogoAPI to a recent commit * Fix PogoAPI to a recent commit * Fix PogoAPI to a recent commit * Added missing method * Various bugfixes * Various bugfixes * Various bugfixes * Various bugfixes * Merging with recent commits * Restored RecycleItemsWorker call * Merged with latest commit * Fixed arguments in method call * Only work on forts when there is space in the bag. Do not switch mode (#1237) * config parsing clean-ups (#1240) * Update the location & location_cache logic (#1217) Previously, the location_cache can't be used, even people set location_cache to TRUE. Everytime, it starts from the initial position. Now the bot check the initial position first. Then check if people set location_cache. If so, read it and update it, otherwise use the initial location. If there is no initial location and no location cache, print msg and exit. * optimize docker usage (#1257) * update Dockerfile; install missed python-protobuf package; add CMD as a way of defining default arguments for the ENTRYPOINT * remove CMD command, default config parameter is also set in pokecli.py * Only catch things that inherit from Exception. Try to prevent KeyboardError from being swallowed. (#1270) * seperate worker for transfering pokemons to professor (#1281) pulled out initial transfer and transfer on catched pokemon to seperate worker * Revert "Transfer poke worker" (#1284) * Added GA in the README.me on dev branch first. * Revert "Added GA in the README.me on dev branch first." This reverts commit 90a17c09971472f89243e8498dc5663292c8212c. * adding random delay between pokemon capture & transfer. #774 (#1225) * adding random delay between pokemon capture & transfer. #774 * improved fix for #774 by adding click_action_delay function * wait time minimum & maximum are passed in from the config now * updated readme & contributors files * updated how action wait times are loaded from config * optimize docker usage (#1257) * update Dockerfile; install missed python-protobuf package; add CMD as a way of defining default arguments for the ENTRYPOINT * remove CMD command, default config parameter is also set in pokecli.py * Removed erroneous line in human_behavior and corrected bad merge resolution * updated configs & changed function name to action_delay * Fixed seperate transfer bug in worker (#1286) * seperate worker for transfering pokemons to professor pulled out initial transfer and transfer on catched pokemon to seperate worker * Fixed bug where config parameters wasnt checked * Initial_transfer renamed to release_pokemons Also updated the configuration argument function to release pokemon runs on tick method which is called after everyt small step the bot takes * added back exception which was missing * Default conf value and fixed typo * fixed typo for pokemon and updated config * added default value if its missing from conf NOTE: transfer conditions are set by "release" parameters in config, however we keep the highest CP of each pokemon, just to be sure we don't empty the bag. * Capital leter * missed to rename on some places Also enabled loggning to easier see why a certain pokemon may not be transfered to professor for candy * Log format update * moved runtime error to evolved_capture statement workaround if anyone should get it... fixing it in future * Anonymous login succ/failed/relogin/logout info to GA. (#1312) * There's a big warning before bot login and 2s wait for exit if the health report is turned on. Anonymous login succ/failed/relogin/logout info without any user account info will be sent to GA. The function is not called to wait Signal system merge. * Fixed page view is not true. * Removed [x] since we ditched it. * Refactoring get_nearest_fort code * Update README.md * Replacing config.mode with capture_pokemon and spin_forts * Removed the GA. * Revert "Removed the GA." This reverts commit 58d7a67e813e4bdc3dd96defc04a7ed1ff76ffa2. * Allow to keep stronger pokemon (#1302) * Allow to keep stronger pokemon. It is woring both with CP and IV * Remove not needed code * Add example of keep_best_iv into configuration * Add delay before pokemon transfering * Resolve merge conflicts * Use config.release_pokemon to determine should we release pokemon or not * Fix a bug * Update config.json.example (#1340) Update config.json.example 4c46ad7e6b41f4d66400aee82be469cdbbb26f80 * Adding the new flags to the config.json example files (#1344) * Add Mr. Mime to config.json.pokemons.example (#1350) Add Mr. Mime to configs/config.json.pokemons.example * Refactoring code into a SpinNearestFortWorker (#1351) * Removing CatchVisiblePokemonWorker's argument of cell (#1352) * Iterate over each worker and treat them the same (#1356) * Fix merge conflicts that caused bugs (#1361) * Merging some unnecessary methods and renaming take_step on the bot to… (#1360) * Merging some unnecessary methods and renaming take_step on the bot to tick * Merging variable definition * Use a more human friendly time format (#1364) * fixed transfer worker api crash (#1369) * fixed transfer worker api crash try catch exception to prevent bot from crashing * better cach error method * prevent call __getitem__ on bool error (#1355) * prevent call __getitem__ on bool error * fix for pr * update sample config to solve bug (#1392) buggy: "catch_above_cp": 0, "catch_above_iv": 0.8, "logic": "and" replaced: "always_catch": true * Added worker for incubating eggs (#1404) * Added worker for incubating eggs * Added options to configuration * Bugfix * Ignoring used eggs and incubators * Using cached inventory instead of getting a fresh copy * Implement proper version of keep_best option (#1395) * Implement proper version of keep_best option * Fix order of pokemons * Change formatting * Display kept pokemons, to be sure that it is working ok * Don't print same info again and again * Remove other worker using * Get back spacing format * Catch transfer worker error (#1423) * fixed transfer worker api crash try catch exception to prevent bot from crashing * better cach error method * Fix for clash between CLI and JSON args (#1420) * makes spin forts and catch pokemon config in json not being overwritten by default cli args value if user dont provide cli args * huge fix to how cli and json parameters are loaded The CLI parameter parser now uses JSON-loaded parameters as first fallback to missing parameters. The second fallback to missing parameters are the default values previously used. This is the perfect handling for making CLI args override JSON configuration only for provided args. Non-provided args that are not found in are set to the default value we think most users are going to like. * overriding config from loaded JSON is not necessary here anymore * trying to fix the auth_service parameters * add mixing cli args fixes that were forgotten * fixing unicode load in location * refactoring parameter configuration to avoid mistakes * changed the order of functions in pokecli.py to follow some guidelines main function comes first all all its children below * small fix to evolve_cp_min * text when pokemon is released improved * Adding a message in the Readme about not supporting gym battles (#1453) * Pokemon plural (#1477) * Fix pokemon plural * Delte old pokemon file * [FEATURE] Api Wrapper to handle connection issues (#1459) * add an api wrapper managing (trying to) handle connections error, needs more testing * refine error testing * import fix * sleep less, lazy bum * change retry parameter as an optional argument * Magikarp twice in release block (#1486) * Fix typo in filename (#1494) * Add name to CONTRIBUTORS * Add name to CONTRIBUTORS * Fix typo in catch_visible_pokemon_worker filename * Add missing renamed catch_visible_pokemon_worker to repository * [Bug fixes] Further checking for the api wrapper response (#1499) * further checking for the api response * make sure to pop the request_callers field first * comment * new PolylineWalker(StepWalker) - [was #990] (#1467) * new PolylineWalker(StepWalker) Refactoring in the context of the new walker/navigator concept Fixes: - PolylineWalker class renamed to Polyline - new class PolylineWalker(StepWalker) - change few tests * fixed imports * Added further release functionality (#1472) * Added functionality to keep Pokemons based on IV and CP at the same time. * Fixed example config and _validate_keep_best_config * Added secondary criteria IV when CP is equal and the contrasting case. * Removed unused import * Use type unicode for argument location (#1503) * Use type unicode for argument location Fix for issue with invalid value for location argument, e.g.: invalid value: u'Pra\xe7a' when it contains special characters like "ç". * Parse location for both command line and json Will now correctly parse location both from command line and JSON file. * Better naming for function to parse unicode str * Added circle avoidance (#1515) * Added circle avoidance * Changed to add_config * Changed path to recent_forts. Put config keys into spin_forts key * cp_min -> evolve_cp_min in config.json.example * Update README.md * annoying bug (#1559) Added if try_cnt > 1: to remove the annoying count in logs due to server latency or something The response that comes back is "52" from api. * Throttling api requests. Reverting log change (#1562) * Egg incubation improvements (#1526) * Moved egg hatching to incubation worker, added feedback for users * old response handling and updated readme * Fix evolve_all and use_lucky_egg (#1541) Fixed issues where evolve_all would not run. Also fixed use_lucky_egg so that it only runs on the first tick. Refactored so the EvolveAllWorker._should_run function requires less logic (optimization) * Event system for logs and web socket communication (#1523) * Event system implementation * some web socket work * refactored the event system for clean ups * added socketio_client to requirements * let's not run event system setup yet and remove some tests * add possibility to set the event level in event system * some event system examples as comments * fixed handler and rudimentary version of real logging handler * better logging * fixing type in example about emitting events * added host and port configuration for websocket server instead of hardcoding them * added flask to requirements.txt * Revert "Egg incubation improvements" (#1565) * Reduce log spam when moving towards forts (#1566) * Fixed import error in api_wrapper (#1561) * Moved egg hatching to incubation worker, added feedback for users (#1568) * Refactoring function to get forts (#1578) * Refactoring function to get forts * Optionally sort by distance * Update pokemon_transfer_worker.py (#1571) * Update pokemon_transfer_worker.py Grammer correction. * Update pokemon_transfer_worker.py * adding support for embedded config keys and fixing circle prevention * CatchVisiblePokemonWorker now catches pokemon from lures (#1591) * CatchVisiblePokemonWorker catches from lures * Fix typos * Allow worker order to be more easily customised in future with PokemonGoBot (#1600) * Make SeenFortWorker top level (#1601) * CatchVisiblePokemonWorker catches from lures * Pull out SpinNearestFort into its own top level worker. Remove pokemon catching behavior * Removing unused reference * Moving MoveToFort to the top level (#1605) * Moving MoveToFort to the top level * Fixing bad import * Consolidate similar meaning configuration keys properly inside another key (#1590) * adding support for embedded config keys and fixing circle prevention * forgot to fix this config * refactor nested config system to support flag likes `--forts.something.anything` This example `--forts.something.anything` would be parsed as `config.forts_something_anything`. And in the JSON config it should be like this: ``` ‘forts’: { ‘something’: { ‘anything’: 1 } } ``` * add fix_nested_config(config) call * update missing usage of `config.avoid_circles` -> `config.forts_avoid_circles` * removed pdb, sorry * fixing buggy merge, i'm sorry * one last fix to nested config for fort spinning * other small fix to nested config * Fixed args PolylineWalker to match the super StepWalker class (#1621) * * Removed pokemongo_bot/polyline_stepper.py - old Stepper() class * Fixed args PolylineWalker to match the super StepWalker class * Added a check to Polynine() point tinitalization, if no route was found then, we will return no points between orig, dest thus will walk in straight line - expected behaviour will teleport in small steps * * fix typo * "evolve_captured" is now using a list instead of a boolean (#1532) * "evolve_captured" is now using a list instead of a boolean, working the same way as "evolve_all" * parse error with format details when "evolve_captured" is not a string, or is the string "true" or "false" * Extract CatchLuredPokemonWorker from PokemonCatchWorker and improved worker order (#1627) * extracted lure catch worker from pokemon catch * removing information less logs * little refactoring to catch lured pokemon worker * Fix 'with' statement mistakes (#1641) * Update catch_visible_pokemon_worker.py * Update __init__.py * using get_cell_ids from pgoapi package * trying to fix annoying log * ok, I give up on log organisation * Randomize `normalized_reticle_size` and `spin_modifier` parameter for `catch_pokemon` api (#1205) * the MoveToFortWorker should always go to the nearest fort (#1666) * Fix transfer worker not triggered for last pokemon (#1664) * Making the SpiralNavigator a worker (#1683) * Making the SpiralNavigator a worker * Passing pylint * Passing linter * fixes Polyline class to handle a case in which google is returning only one point (#1674) * Fixes: https://github.com/th3w4y/PokemonGo-Bot/issues/27 * Fixes: PolylineStepWalker walks for only one seconds #28 https://github.com/th3w4y/PokemonGo-Bot/issues/28 by adding a while destination nat reached loop * fixes typo * Revert "Making the SpiralNavigator a worker" (#1698) * feat: show xp after catching pokemons (#1700) * Update config.json.pokemon.example (#1711) config.json.pokemon.example was missing several fields present in config.json.example * Huge clean-up: PEP8, sort imports, remove deprecated and unused imports (#1697) * SoftBan Worker (#1724) * created a softban worker * only delete key from dict if it is there * pep8 stuff * Improve docker usage; use docker-compose for starting the PokemonGo-Bot ecosystem (#1669) * update Dockerfile; install missed python-protobuf package; add CMD as a way of defining default arguments for the ENTRYPOINT * remove CMD command, default config parameter is also set in pokecli.py * improve docker usage; add single container run for the webUI; add docker-compose.yml for starting the bot ecosystem with one command * fix "How to run with Docker" link * fix timezone setting: send timezone arg to the docker image build process, e.g. "docker build --build-arg timezone=Europe/Berlin -t pokemongo-bot ." * adding a duplicate of SeenFortWorker before MoveToFortWorker this ensures we interact with forts while we are moving to other forts * Show Pokestop names (#1671) * Restore the ability for a user to see Pokestop names. Default to off. * Use the add_config function for forts.show_name (now default to true) * Move fort_details function into cell_workers init module * Forgot to pass bot reference * Catching lured pokemon should use same fort_details API * REmove config option. Always show Pokestop name. * Move away from KeyError handling as per TheSaviour's suggestion * fixing wrong import * No longer caching things on the worker. Pulling straight from the bot instance (#1747) * Creating an instance of the workers only on startup (#1750) * Don't try to release pokemons in forts (#1751) * * Always report session summary even on crash (#1759) * Prevent crash checking session (#1754) * Prevent crash when check session * Fix function call * [FIX] use_lucky_egg (#1774) Changes to the tick_count caused the use_lucky_egg to not run. Since the tick_count is incremented prior to running the workers, the tick_count will be 1 on the first tick * add a flag to enable user to choose if he wants to walk to spin forts (#1772) * add a flag to enable user to choose if he wants to walk to spin forts that are far away * updated config example * Display Fort Name instead of Fort ID in Log (#1801) Displays the fort name in the CLI instead of the fort id. Easier to read, better to look at than a hash. * Update incubate_eggs_worker.py (#1862) Fix: variable 'pokemon_data' referenced before assignment * Improved some code formatting & fixed unicode issue with the logger. (#1839) * Improved some code formatting & fixed unicode issue with the logger. * Corrected formatting of log method & improved formatting of spiral_navigator.py * Upgrade the capture logic for VIP pokemons! (#1807) * This fix a small bug when user didn't update their config file for VIP setting (#1874) * [FEATURE] Path Navigator (#1457) Adds a navigator that walks along specified points. * fixing a typo that causes a NameError exception (#1898) * Adding a TreeConfigBuilder and tests (#1901) * Adding a TreeConfigBuilder and tests * Adding mock to the requirements * not actually using mock or patch * Egg Incubation - IV fix and UnboundLocalError fix (#1777) * fixes for ivs and bad var * Custom response/early return for error * added check for blank ids * added temp lists for duplication mitigation * Removing Worker suffix on workers (#1914) * Renaming more workers to make grammatical sense (#1915) * Improved Path Navigator, Now Supports geopositioning resolution (#1917) * Refactored Path Navigator, now supports geopositioning resolution * Update path example config, for new format * Fixed typo in dict * Fixed Ref * Possibility to set another config with run script (#1899) * Location cache check. If start position differs, don't use the cache. (#1932) * Making the navigators workers (#1933) * Adding the navigator to the list of workers (#1950) * Updated item_filter in config.json.example to use item names instead item id's (#1733) * Updated item_filter in config.json.example to use item names instead of item id's * Config.json item_list verification * Merge remote-tracking branch 'upstream/dev' into dev Conflicts: pokemongo_bot/cell_workers/recycle_items.py * Adding recycle_items back Changed error return type * Configure the tasks from config.json (#1956) * Configure the tasks from config.json * Linking error to wiki * Removing config for catch_pokemon (#1963) * Loop over an array of old flags (#1964) * Removing config forts_spin * Removing hatch_eggs from config * One more for hatch_eggs * Removing config for release_pokemon * One more for release_pokemon * Removing config for softban_fix * Removing config for forts.spin * Removing config for forts.move_to_spin * Supporting task level configuration (#1979) * Supporting task level configuration * Updating sample config files * Providing example of how to configure tasks in the example configs * Adding a task base class (#1983) * Re-enable item ID's in the item_filter. (#1986) * Allows users to enter both item ID's and item names in the item_filter. * no message * Allow to collect level up rewards (#2004) * Foundation for remote control of the bot over websocket (#2000) * improved websocket and logging handler * added support for remote command execution through websocket and a player_info call example * adding a missing variable * only execute remote command if it exists and is callable, else return command not found * wait forever instead of 5 secs in an infinite loop * Tasks now extend a base task (#2007) * Moving evolve_speed to task configuration * Moving use_lucky_egg to be a task configuration * Fixing bug in evolve all. Fixes #2019 * Removing unused navigator switch * Avoid transferring favorite pokemons (#2038) * Move follow path task config (#2044) * Refactor (#1587) * fixing `.get` call (closes #2082) * Log location as a str (#1825) We already have location as an encoded str in location_str and most calls to log pass a string as argument, so this is a bit more consistent. * Update README.md * Giving errors when specifying navigator cli arguments (#2126) * [DEV FEATURE] Test framework .... beginning (#1682) * add mock and nose to the dependencies * added unit tests for the api_wrapper * add testing to travis build * fixing path ? * pylint error fixed * adding myself to contributors * add test for the step_walker * add runtime error for big distances * change travis, nosetests should look for tests in all the folders * Getting rid of nose, rename some files, add 'timeout_decorator' to the requirements * update travis.yml * changed run_tests script from bash file to python file * revert file changes * skipping failing test * fix another test * some style/import improvment * revert SKIP_TIMEOUT * remove run_tests.py * move tests cases into main test folder * refactor some code api_wrapper_test * refactor and location parser * test is failing add a FIXME tag * location is now unicode friendly * Fix throw type always normal when trying to catch pokemon (#2130) * Warns if there aren't sufficient space left for loot. (#2137) * Warns if there aren't sufficient space left for loot. SpinFort will terminate silently, and users will not notice that SpinFort is skipping due to the lack of space. As a result, it sends out requests to Niantic even faster than before becuase tasks are looping faster and doing noops. More server busy (error 52) errors appear in the log without an explanation of what's actually going on. * improve readability of should_run for SpinFort and MoveToFort * [Feature] Detect maximum cluster and move (#1993) * adapted to new commits * added config changed gitignore * added config * locked versions * typo * account for task management change * moved find_cluster to utils. follow_cluster now 1 task * added test for follow_cluster * added mock requirements * trying to get travis to build * trying to get travis to build * added search for lured cluster functionality * adapted tests * removed double specification * only use berries on VIP pokemon if catch rate is less than 90% #2135 (#2138) * [FIX-Config] 'use_lucky_egg' should not be true in the exmple config file (#2105) * Update README.md * Added check for valid keep_best_iv amount (#2150) * Cluster Selection so it doesn't jump from cluster to cluster (#2153) * added secondary criteria so it doesn't jump from cluster to cluster when they're equally large * added secondary criteria so it doesn't jump from cluster to cluster when they're equally large * updated cluster example * Adding an Anyball item to the release config (#2140) Adding an Anyball item to the release config * removed wrong log (#2160) * Revert "Adding an Anyball item to the release config" (#2166) * This should fix issue: (#2185) MoveToFort(self.bot).work() TypeError: __init__() takes exactly 3 arguments (2 given) * Better enforce rules about Pokemons to retain. (#2073) Rules about maximum CP and IV to transfer Pokemons are applied also when keeping the best ones. * Fix incorrect config value for HandleSoftban. (#2191) * Moving item_filter to be a task level configuration * Moving evolve_all to be a task level configuration * Moving evolve_cp_min to be a task level configuration * Added nickname worker (#1850) * Adding a SleepSchedule worker. Pause for some time every day (#2193) * Added Sleeper worker * changed Sleep worker name to SleepSchedule * fixed wrong import names * changed name in log * Removing nickname pokemon from the example config * Remove MoveToFort from the FollowPath example config (#2203) FollowPath will not work properly while also using MoveToFort. * Using the logger instead of print in EvolveAll * fixed pokemon transfer so that "keep_best_*" filters can work again without requiring to be combined with "release_below_*" rules (#2215) * Let the user know that the maps api key exceeded its limits. (#1989) * Change FortID to FortName (#2249) Making it more human readable. I did not test this change, just used the same field from movetofort line 41 and spin fort line 33. * Request meta cell data once every 5 seconds (#2171) This solve the Niantic "scan for pokemon" throttling without making the bot very slow. * Use Default map_object_cache_time if not specified * Change egg hatching text (#2258) * [FIX] Improper use of exception (#2246) * adapted improper exception use * beautified * fixed logic error * Add optional simple lure attraction feature (#2257) * Add lure attraction params in default config * Update CONTRIBUTORS.md * Add simple lure attraction feature (move2fort) * Update move_to_fort.py * Dev - Fixed the loss of fort data (updated) (#2269) * Keep fort data even if the server returns no fort data. Also replaced redundant code. * Making sure we only save fort data if the server returned multiple forts. * Update web to latest master commit (#2274) The current dev commit has an issue where it doesn’t show the number of candies. * Add missing curly bracket (#2282) Added missing curly bracket to tasks>MoveToFort>config * added param in config.json.pokemon.example. * web submodule updated to latest commit (#2289) * Modify SpiralTask to use 70m as stepsize and diameter as step_count (#2194) * Lowered the stepsize in Spiral navigator to more accurate 70m * Moved max_steps to task configuration and changed it to diameter * Added diameter to configuration example * Bugfix * Removed max_steps from cli * Added max_steps as legacy configuration * Made step size of follow_spiral more configureable * Changed default value for diameter * This is just a temp fix, The one added the configure param need make sure it's really work. * Fix instance where evolve_all is unicode - fixes #2281 (#2305) * Fix instance where evolve_all is unicode * Test for isinstance basestring rather than Unicode || str * [Feature] added keep pokemon for batch evolution (#2255) * added keep_for_evo * accounted for non evolable pokemon * additional logging * additional logging * moved get_candies to utils * disregard 2nd stage evolution pokemon * added sample configs * Supporting sending requests through an HTTP proxy (#2358) * Added proxy support Added proxy support from config.json and command line. with parameters: -prxip | --proxy_ip or proxy_ip from config.json for ipv4 format as string "aaa.bbb.ccc.ddd" -prxp| --proxy_port or proxy_por from config.json as int from 1 to 65536. * Added proxy support Added two additional parameters (proxy_ip and proxy_port) to add support for proxy. proxy_ip must be as string of ipv4 format: "aaa.bbb.ccc.ddd" , proxy_port must be a int from 1 to 65536, if proxy_ip is null or set to "" and proxy port is null or set to 0 proxy will be ignored and not used. * Moved proxy function to a method. Moved proxy function to a method. * Changed the name of method Changed from set_proxy_if_exists to setup_proxy * Revert "Dev Proxy support" (#2374) * Revert "[Feature] added keep pokemon for batch evolution" (#2380) * Adapt code to new PGoApi Code (#2357) * wip: fixing imports and tests * tests are passing * add tests, and modify calls to api accordingly * fix login bug * fix rebase * fix import error * Handle ThrottlingException * fix relogging errors * Refactor evolve_all worker (#2244) * Refactor evolve_all worker - Remove transfer of evolved pokemon (should be handle by transfer task) - Add order_by config flag to choose to evolve by iv or cp (default: cp) - Add evolve_iv_min as threshold for evolve by iv (order by cp under threshold) - Fix _validate_config not called before - Get candy list to test if enough candy in the bag - Filter out pokemon which can't be evolved * remove unnecessary debug lines * Add missing candy name * Use uncached inventory to have up to date amount of candy and list of pokemon * Fix candy name Add missing candies * Fix evolving logic: - replace "order_by" by "first_evolve_by" to choose if we prioritize "cp" or "iv" (default: "cp") - replace "evolve_cp_min" by "evolve_above_cp" (default: 500) - replace "evolve_iv_min" by "evolve_above_iv" (default: 0.8) - add "logic" to choose if we "evolve_above_cp" and/or "evolve_above_iv" - update config file * Rename EvolveAll to EvolvePokemon task name * Add error warning about task renaming * Add a test about tasl renaming * Fix task renaming warning * Update new api wrapper * delaying errors, and reducing noise (#2393) * Removed max_steps in the config pokemon sample. Also added FollowSpiral's new options (#2342) * Display stats in the terminal title (#2252) * Added UpdateTitleStats worker * Added UpdateTitleStats worker * Fixed return inconsistency in work method * :rocket: Massively improved pylint rate Cleaned ctypes unnecessary imports Moved initialization inside __init__ method * Fixed incorrect default value for min_interval * Added support for cygwin on Windows * Catch UnexpectedResponseException and retry (#2407) * Catch UnexpectedResponseException and retry * New func for UnexpectedResponse * Fixed merge conflicts. * Randomize spins for softban #2247 (#2253) * Randomize spins for softban #2247 * Update handle_soft_ban.py * fix(docker): correct web config file path (#2350) * Allow for 3-7 decimal points for coordinates (#2402) Some exports only provide three decimal point accuracy, and sometimes 4.440000 turns into 4.44, need to adjust the regex. * PokemonGo-Map Synergy (#1992) * Feature: Use PokemonGo-Map sqlite db to catch pokemon near you * added example config for move_to_map_pokemon * adapted new config format * Automatically update Map position * remove pokemon when encountered early * forgot to remove a log * minor fix * updated example config and added ignore config * change ignore config to a list * teleport to pokemon if walk option is 0 * added snipe option * Teleport back after sniped pokemon was caught * proper sniping * mark sniped pokemon as caught * forgot to remove print * minor bug fix * ignore max_distance when sniping * prioritize VIPs in a 10km radius * better prioritize vips * syntax error fix * set base priority for vips * move map config example to seperate file * use web api instead of sqlite db * fix datetime format * huge code cleanup * forgot to snipe * add vips to catch as default * default priority for vips * only mark pokemon as caught when it really was caught * bugfix * bugfix #2 * i should go to bed * add option to disable map update * updated example map config to match default example * improve pylint result, fix catch recognition * more code clean up * better config example * dump caught pokemon to json file to prevent targeting them on restart * check if we got pokeballs to use * remove print * fix item_inventory_count returnin None instead of 0 * if we only have ultraballs and the target is not a vip don't snipe/walk * remove gender symbols * fix Mr. Mime * vip wrong order fix * bugfix * log error when JSON decoding fails * handle base64 error * Return type None on nested call was breaking details display (#2416) * Adding a section on analytics and metrics to the Readme (#2434) * Fix to display stats on iterm2 terminal (#2440) * Fix #2442 - should_retry_throttle isn't defined (#2461) * Fix #2442 Variables weren't correctly defined * Fix typo * Replace all `logger.log` calls with events! (#2173) * bye bye `logger.log`, hello event system! * fixing travis build * trying to fix travis build * test fixes * updating remaining `logger.log` calls that should be replaced * typo * typos in IncubateEggs event * improved fort loot event data * fixing update_location event's distance unit * fixing some events and log stuff * adding missing fort_name parameter to lured_pokemon_found event * fixing a variable inside an event formatted string * fixing typos and utf8 * trying to fix tests with regards to float precision * adding command to print all registered events and their parameters * fixing tests yet again * trying to fix unicode issues, arrgh!!! * added a move to lured fort event * better distance text in move to fort and fixing utf8 in spin fort task * removing print from websocket server * start embedded server before creating the socketio_handler * I hate unicode * rename and sleep events * refactoring in how we emit events to avoid code repetition * PokemonCatch task inherits from BaseTask * go away, dirty logger.log! * pep8 and removed logging handler name attribute * good bye for the remaining logger.log calls * bye logger module * no more logger imports! * removed last few loggers * removing secret file and fixed variable name in follow cluster * fixing kwargs for event emit * trying to fix unicode handling one more time * now it works! * fixing more logs and removing debug unicode string * no logs on websocket server yet * adding a script to start a standalone websocket server * more adjusted in websocket to support multiuser * adding a fallback to logger.log issues a very verbose deprecation warning * putting back compatibility with json based web ui * correct parsing evolve_all (#2455) * correct parsing evolve_all Previously, ``` "evolve_all": "Pidgey, Caterpie, Weedle", ``` would only evolve Pidgey. This PR fix that. * fix parsing evolve_captured * Remove max_steps from examples and set EvolveAll to EvolvePokemon (#2430) * Adding Raven to send exception reports to Sentry (#2514) * Adding Raven to send exception reports to Sentry * Removing test exception * Removing incompatible python3 analytics library * Using logger.log * Using the correct logger * changing license from MIT to GPLv3 * Updated README.md to state bot status (#2586) * Added bot broken message at top of page. * Changed REAME.md to mirror #2590 * Reordered * Handling KeyboardInterrupt and some other exceptions (#2599) --- .dockerignore | 4 + .gitignore | 36 +- .gitmodules | 2 +- .travis.yml | 4 +- CONTRIBUTORS.md | 53 + Dockerfile | 12 +- LICENSE | 674 ++++++++ README.md | 202 +-- config.json.example | 17 - configs/config.json.cluster.example | 123 ++ configs/config.json.example | 131 ++ configs/config.json.map.example | 361 +++++ configs/config.json.path.example | 101 ++ configs/config.json.pokemon.example | 357 +++++ configs/path.example.json | 6 + data/pokemon.json | 2 +- docker-compose.yml | 20 + install.sh | 19 + pokecli.py | 521 ++++-- pokemongo_bot/__init__.py | 1245 ++++++++++----- pokemongo_bot/api_wrapper.py | 157 ++ pokemongo_bot/cell_workers/__init__.py | 22 +- pokemongo_bot/cell_workers/base_task.py | 30 + .../cell_workers/catch_lured_pokemon.py | 51 + .../cell_workers/catch_visible_pokemon.py | 48 + .../cell_workers/collect_level_up_reward.py | 74 + .../cell_workers/evolve_all_worker.py | 240 --- pokemongo_bot/cell_workers/evolve_pokemon.py | 168 ++ pokemongo_bot/cell_workers/follow_cluster.py | 85 + pokemongo_bot/cell_workers/follow_path.py | 103 ++ pokemongo_bot/cell_workers/follow_spiral.py | 116 ++ pokemongo_bot/cell_workers/handle_soft_ban.py | 69 + pokemongo_bot/cell_workers/incubate_eggs.py | 207 +++ .../cell_workers/initial_transfer_worker.py | 75 - pokemongo_bot/cell_workers/move_to_fort.py | 150 ++ .../cell_workers/move_to_fort_worker.py | 40 - .../cell_workers/move_to_map_pokemon.py | 197 +++ .../cell_workers/nickname_pokemon.py | 101 ++ .../cell_workers/pokemon_catch_worker.py | 689 +++++--- pokemongo_bot/cell_workers/recycle_items.py | 63 + .../cell_workers/seen_fort_worker.py | 138 -- pokemongo_bot/cell_workers/sleep_schedule.py | 108 ++ pokemongo_bot/cell_workers/spin_fort.py | 157 ++ .../cell_workers/transfer_pokemon.py | 233 +++ .../cell_workers/update_title_stats.py | 233 +++ pokemongo_bot/cell_workers/utils.py | 148 +- pokemongo_bot/constants.py | 2 + pokemongo_bot/event_handlers/__init__.py | 2 + .../event_handlers/logging_handler.py | 18 + .../event_handlers/socketio_handler.py | 30 + pokemongo_bot/event_manager.py | 65 + pokemongo_bot/health_record/__init__.py | 3 + pokemongo_bot/health_record/bot_event.py | 68 + pokemongo_bot/human_behaviour.py | 39 +- pokemongo_bot/lcd.py | 12 +- pokemongo_bot/logger.py | 40 +- pokemongo_bot/metrics.py | 113 ++ pokemongo_bot/polyline_stepper.py | 56 - pokemongo_bot/socketio_server/__init__.py | 0 pokemongo_bot/socketio_server/app.py | 32 + pokemongo_bot/socketio_server/runner.py | 34 + pokemongo_bot/step_walker.py | 65 + pokemongo_bot/stepper.py | 158 -- pokemongo_bot/test/__init__.py | 0 pokemongo_bot/test/follow_cluster_test.py | 42 + .../test/resources/example_forts.pickle | 1415 +++++++++++++++++ pokemongo_bot/test/sleep_schedule_test.py | 107 ++ pokemongo_bot/test/socketio-client.py | 15 + pokemongo_bot/tree_config_builder.py | 36 + .../{polyline_walker => walkers}/__init__.py | 1 + .../polyline_generator.py} | 72 +- .../polyline_generator_tester.py} | 10 +- pokemongo_bot/walkers/polyline_walker.py | 31 + pokemongo_bot/websocket_remote_control.py | 53 + pokemongo_bot/worker_result.py | 3 + pylint-recursive.py | 60 + release_config.json.example | 174 -- requirements.txt | 12 +- run.sh | 18 + setup.py | 1 - tests/__init__.py | 23 + tests/api_wrapper_test.py | 123 ++ tests/base_task_test.py | 40 + tests/location_parser_test.py | 33 + tests/step_walker_test.py | 73 + tests/tree_config_builder_test.py | 85 + tests/update_title_stats_test.py | 131 ++ travis-pythoncheck.py | 48 - web | 2 +- ws_server.py | 26 + 90 files changed, 9036 insertions(+), 1927 deletions(-) create mode 100644 .dockerignore create mode 100644 CONTRIBUTORS.md create mode 100644 LICENSE delete mode 100644 config.json.example create mode 100644 configs/config.json.cluster.example create mode 100644 configs/config.json.example create mode 100644 configs/config.json.map.example create mode 100644 configs/config.json.path.example create mode 100644 configs/config.json.pokemon.example create mode 100644 configs/path.example.json create mode 100644 docker-compose.yml create mode 100755 install.sh mode change 100755 => 100644 pokecli.py create mode 100644 pokemongo_bot/api_wrapper.py create mode 100644 pokemongo_bot/cell_workers/base_task.py create mode 100644 pokemongo_bot/cell_workers/catch_lured_pokemon.py create mode 100644 pokemongo_bot/cell_workers/catch_visible_pokemon.py create mode 100644 pokemongo_bot/cell_workers/collect_level_up_reward.py delete mode 100644 pokemongo_bot/cell_workers/evolve_all_worker.py create mode 100644 pokemongo_bot/cell_workers/evolve_pokemon.py create mode 100644 pokemongo_bot/cell_workers/follow_cluster.py create mode 100644 pokemongo_bot/cell_workers/follow_path.py create mode 100644 pokemongo_bot/cell_workers/follow_spiral.py create mode 100644 pokemongo_bot/cell_workers/handle_soft_ban.py create mode 100644 pokemongo_bot/cell_workers/incubate_eggs.py delete mode 100644 pokemongo_bot/cell_workers/initial_transfer_worker.py create mode 100644 pokemongo_bot/cell_workers/move_to_fort.py delete mode 100644 pokemongo_bot/cell_workers/move_to_fort_worker.py create mode 100644 pokemongo_bot/cell_workers/move_to_map_pokemon.py create mode 100644 pokemongo_bot/cell_workers/nickname_pokemon.py create mode 100644 pokemongo_bot/cell_workers/recycle_items.py delete mode 100644 pokemongo_bot/cell_workers/seen_fort_worker.py create mode 100644 pokemongo_bot/cell_workers/sleep_schedule.py create mode 100644 pokemongo_bot/cell_workers/spin_fort.py create mode 100644 pokemongo_bot/cell_workers/transfer_pokemon.py create mode 100644 pokemongo_bot/cell_workers/update_title_stats.py create mode 100644 pokemongo_bot/constants.py create mode 100644 pokemongo_bot/event_handlers/__init__.py create mode 100644 pokemongo_bot/event_handlers/logging_handler.py create mode 100644 pokemongo_bot/event_handlers/socketio_handler.py create mode 100644 pokemongo_bot/event_manager.py create mode 100644 pokemongo_bot/health_record/__init__.py create mode 100644 pokemongo_bot/health_record/bot_event.py create mode 100644 pokemongo_bot/metrics.py delete mode 100644 pokemongo_bot/polyline_stepper.py create mode 100644 pokemongo_bot/socketio_server/__init__.py create mode 100644 pokemongo_bot/socketio_server/app.py create mode 100644 pokemongo_bot/socketio_server/runner.py create mode 100644 pokemongo_bot/step_walker.py delete mode 100644 pokemongo_bot/stepper.py create mode 100644 pokemongo_bot/test/__init__.py create mode 100644 pokemongo_bot/test/follow_cluster_test.py create mode 100644 pokemongo_bot/test/resources/example_forts.pickle create mode 100644 pokemongo_bot/test/sleep_schedule_test.py create mode 100644 pokemongo_bot/test/socketio-client.py create mode 100644 pokemongo_bot/tree_config_builder.py rename pokemongo_bot/{polyline_walker => walkers}/__init__.py (51%) rename pokemongo_bot/{polyline_walker/polyline_walker.py => walkers/polyline_generator.py} (51%) rename pokemongo_bot/{polyline_walker/polyline_tester.py => walkers/polyline_generator_tester.py} (83%) create mode 100644 pokemongo_bot/walkers/polyline_walker.py create mode 100644 pokemongo_bot/websocket_remote_control.py create mode 100644 pokemongo_bot/worker_result.py create mode 100644 pylint-recursive.py delete mode 100644 release_config.json.example create mode 100755 run.sh create mode 100644 tests/__init__.py create mode 100644 tests/api_wrapper_test.py create mode 100644 tests/base_task_test.py create mode 100644 tests/location_parser_test.py create mode 100644 tests/step_walker_test.py create mode 100644 tests/tree_config_builder_test.py create mode 100644 tests/update_title_stats_test.py delete mode 100644 travis-pythoncheck.py create mode 100755 ws_server.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..b08db7b967 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.idea +.git* +**/*config.json +**/*userdata.js \ No newline at end of file diff --git a/.gitignore b/.gitignore index cb30b978fa..a12509c322 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,6 @@ var/ .pydevproject .settings/ - # Installer logs pip-log.txt pip-delete-this-directory.txt @@ -98,19 +97,26 @@ ENV/ .idea/ # Personal load details -config.json src/ -info.json -inventory.json -pokedex.json -web/catchable.json -web/catchable-*.json -web/location-*.json -web/inventory-*.json -web/location.json -web/userdata.js +web/ data/last-location*.json -data/catch-ignore.yml -release_config.json -web/userdata.js -location.json +data/cells-*.json +data/map-caught-*.json + +# Multiple config +configs/* +!configs/config.json.example +!configs/release_config.json.example +!configs/config.json.pokemons.example +!configs/config.json.pokemon.example +!configs/config.json.path.example +!configs/config.json.map.example +!configs/path.example.json +!config.json.cluster.example + +# Virtualenv folders +bin/ +include/ + +# Pip check file +pip-selfcheck.json diff --git a/.gitmodules b/.gitmodules index 7930970692..612529e7ad 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "origin"] +[submodule "web"] path = web url = https://github.com/OpenPoGo/OpenPoGoWeb.git \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 3a9d0b9443..aef60fa3c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,4 +10,6 @@ addons: install: - pip install -r requirements.txt - pip install pylint -script: "python travis-pythoncheck.py" +script: + - python pylint-recursive.py + - python -m unittest discover -v -p "*_test.py" diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000000..dfd8a24af2 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,53 @@ +## Contributors + * eggins [first pull request] + * crack00r + * ethervoid + * Bashin + * tstumm + * TheGoldenXY + * Reaver01 + * rarshonsky + * earthchie + * haykuro + * 05-032 + * sinistance + * CapCap + * mzupan + * gnekic(GeXx) + * Shoh + * luizperes + * brantje + * VirtualSatai + * dmateusp + * jtdroste + * msoedov + * Grace + * Calcyfer + * asaf400 + * guyz + * DavidK1m + * budi-khoirudin + * riberod07 + * th3w4y + * Leaklessgfy + * steffwiz + * pulgalipe + * BartKoppelmans + * phil9l + * VictorChen + * AlvaroGzP + * fierysolid + * surfaace + * surceis + * SpaceWhale + * klingan + * reddivision + * DayBr3ak + * kbinani + * mhdasding + * MFizz + * NamPNQ + * z4ppy.bbc + * matheussampaio + * Abraxas000 + * lucasfevi diff --git a/Dockerfile b/Dockerfile index a1bdd61406..58c45cd02f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,13 @@ FROM python:2.7-onbuild -ENTRYPOINT ["python", "pokecli.py"] +ARG timezone=Etc/UTC +RUN echo $timezone > /etc/timezone \ + && ln -sfn /usr/share/zoneinfo/$timezone /etc/localtime \ + && dpkg-reconfigure -f noninteractive tzdata + +RUN apt-get update \ + && apt-get install -y python-protobuf + +VOLUME ["/usr/src/app/web"] + +ENTRYPOINT ["python", "pokecli.py"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..9cecc1d466 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index d5b44ae8a1..8fc2ed8741 100644 --- a/README.md +++ b/README.md @@ -1,174 +1,66 @@ -

- - Logo - -

- -

- Slack -

# PokemonGo-Bot -The Pokemon Go Bot, baking with community. -## Niantic Changes, currently there's big progress, we are waiting for the API update pkmngodev/Unknown6. +PokemonGo bot is a project created by the [PokemonGoF](https://github.com/PokemonGoF) team. +The project is currently setup in two main branches. `dev` and `master`. -Niantic have changed API responses, meaning that this bot and anything accessing the API though POGOProtos is currently broken. A number of developers from /r/pokemondev are working to address this and come up with a fix for this issue, find the [current status here](https://www.reddit.com/r/pokemongodev/comments/4w1cvr/pokemongo_current_api_status/) -## Help Needed on [Desktop Version](https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) -## Help with the project [Dev Bot](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Develop-PokemonGo-Bot) -## Project Chat -We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) -## Breaking Changes -You need modify config.json (config.json.pokemon.example for example) then python pokecli.py --config configs/config.json +## Niantic Changes + Niantic have changed API responses, meaning that this bot and anything accessing the API though POGOProtos is currently broken. A number of developers from /r/pokemondev are working to address this and come up with a fix for this issue, find the [current status here](https://www.reddit.com/r/pokemongodev/comments/4w1cvr/pokemongo_current_api_status/) -[More details about config file](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files) -Please clean up your old clone if you have issue, and following the [install instruction](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation). +We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) -## About dev/master Branch -Dev branch has the most up-to-date features, but be aware that there might be some broken changes. Your contribution and PR for fixes are warm welcome. -Master branch is the stable branch. -No PR on master branch to keep things easier. ## Table of Contents -- [Project Chat](#project-chat) - [Features](#features) -- [TODO List](#todo-list) -- [Usage](#usage) +- [Wiki](#wiki) - [Credits](#credits) - [Donation](#donation) + ## Features - * Search Fort (Spin Pokestop) - * Catch Pokemon - * Release low cp pokemon - * Walking as you - * Limit the step to farm specific area for pokestops - * Use the ball you have to catch, don't if you don't have - * Rudimentary IV Functionality filter - * Auto switch mode (Full of item then catch, no ball useable then farm) - * Ignore certain pokemon filter - * Use superior ball types when necessary - * When out of normal pokeballs, use the next type of ball unless there are less than 10 of that type, in which case switch to farm mode - * Drop items - * Pokemon catch filter - * Google Map API key setup - * Show all objects on map (In Testing) - * Evolve pokemons (Code in, Need input, In Testing) - * Incubate eggs - * Hatch eggs - * Pokemon transfer filter - -## TODO List - -- [ ] Standalone Desktop APP +- [x] GPS Location configuration +- [x] Search Pokestops +- [x] Catch Pokemon +- [x] Determine which pokeball to use (uses Razz Berry if the catch percentage is low!) +- [x] Exchange Pokemon as per configuration +- [x] Evolve Pokemon as per configuration +- [x] Auto switch mode (Inventory Checks - switches between catch/farming items) +- [x] Limit the step to farm specific area for pokestops +- [x] Rudimentary IV Functionality filter +- [x] Ignore certain pokemon filter +- [x] Adjust delay between Pokemon capture & Transfer as per configuration +- [ ] Standalone Desktop Application +- [x] Hatch eggs +- [x] Incubate eggs - [ ] Use candy -- [ ] Softban Bypass (In Development) +- [ ] Inventory cleaner ## Gym Battles This bot takes a strong stance against automating gym battles. Botting gyms will have a negative effect on most players and thus the game as a whole. We will thus never accept contributions or changes containing code specific for gym battles. -## Installation -[Getting started guide](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Getting-Started) -[Jump right into installing](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation) - -### Note on virtualenv -We recommend you use virtualenv, not only will this tool keep your OS clean from all the python plugins. -It also provide an virtual space for more than 1 instance! - -### Protobuf 3 installation Notes - -- OS X: `brew update && brew install --devel protobuf` -- Windows: Download protobuf 3.0: [here](https://github.com/google/protobuf/releases/download/v3.0.0-beta-4/protoc-3.0.0-beta-4-win32.zip) and unzip `bin/protoc.exe` into a folder in your PATH. -- Linux: `sudo apt-get install python-protobuf` - -### Note on branch -Please keep in mind that master is not always up-to-date whereas 'dev' is. In the installation note below change `master` to `dev` if you want to get and use the latest version. - -Make sure you install the following first: -[Requirements](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation) - -### Google Maps API Bot Tracker -[Wiki on using the bot web folder](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page) - -### FAQ -[Tips & Tricks](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ) - -## How to run with Docker -[Wiki on how to use Docker](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/How-to-run-with-Docker) - -## How to add/discover new API -The example is [here](https://github.com/PokemonGoF/PokemonGo-Bot/commit/46e2352ce9f349cc127a408959679282f9999585) - -1. Check the type of your API request in [POGOProtos](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/RequestType.proto) For example: RECYCLE_INVENTORY_ITEM - -2. Convert to the api call in pokemongo_bot/__init__.py, RECYCLE_INVENTORY_ITEM change to self.api.recycle_inventory_item - - ```python - def drop_item(self,item_id,count): - self.api.recycle_inventory_item(...............) - ``` - -3. Where is the param list? - You need check this [Requests/Messages/RecycleInventoryItemMessage.proto](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/Messages/RecycleInventoryItemMessage.proto) - -4. Then our final api call is - - ```python - def drop_item(self,item_id,count): - self.api.recycle_inventory_item(item_id=item_id,count=count) - inventory_req = self.api.call() - print(inventory_req) - ``` -5. You can now debug on the log to see if get what you need - -## FAQ -[Wiki Link](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ) - -### What's IV ? -Here's the [introduction](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Pokemon-IV) -Research Website [Nice Tool](https://thesilphroad.com/research) - -### What are the Item ID -[Wiki](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Item-ID's) - -##Softban -[Wiki](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Softban) - ---------- -## Contributors (Don't forget add yours here when you create PR) - * eggins -- The first pull request :) - * crack00r - * ethervoid - * Bashin - * tstumm - * TheGoldenXY - * Reaver01 - * rarshonsky - * earthchie - * haykuro - * 05-032 - * sinistance - * CapCap - * mzupan - * gnekic(GeXx) - * Shoh - * luizperes - * brantje - * VirtualSatai - * dmateusp - * jtdroste - * msoedov - * Grace - * Calcyfer - * asaf400 - * guyz - * DavidK1m - * budi-khoirudin - * riberod07 - * th3w4y - * Leaklessgfy - * GregTampa - * AlexRatman - -------- +## Analytics +This bot is very popular and has a vibrant community. Because of that, it has become very difficult for us to know how the bot is used and what errors people hit. By capturing small amounts of data, we can prioritize our work better such as fixing errors that happen to a large percentage of our user base, not just a vocal minority. + +Our goal is to help inform our decisions by capturing data that helps us get aggregate usage and error reports, not personal information. To view the code that handles analytics in our master branch, you can use this [search link](https://github.com/PokemonGoF/PokemonGo-Bot/search?utf8=%E2%9C%93&q=BotEvent). + +If there are any concerns with this policy or you believe we are tracking something we shouldn't, please open a ticket in the tracker. The contributors always intend to do the right thing for our users, and we want to make sure we are held to that path. + +If you do not want any data to be gathered, you can turn off this feature by setting `health_record` to `false` in your `config.json`. + +## Wiki +All information on [Getting Started](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Getting-Started) is available in the [Wiki](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/)! +- __Installation__ + - [Requirements] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#requirements-click-each-one-for-install-guide) + - [How to run with Docker](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/How-to-run-with-Docker) + - [Linux] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-linux) + - [Mac] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-mac) + - [Windows] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-windows) +- [Develop PokemonGo-Bot](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Develop-PokemonGo-Bot) +- [Configuration-files](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files) +- [Front end web module - Google Maps API] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page)) +- [Docker Usage](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ#how-to-run-with-docker) +- [FAQ](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ) + +To ensure that all updates are documented - [@eggins](https://github.com/eggins) will keep the Wiki updated with the latest information on installing, updating and configuring the bot. + ## Credits - [tejado](https://github.com/tejado) many thanks for the API - [Mila432](https://github.com/Mila432/Pokemon_Go_API) for the login secrets diff --git a/config.json.example b/config.json.example deleted file mode 100644 index 3245708e6d..0000000000 --- a/config.json.example +++ /dev/null @@ -1,17 +0,0 @@ -{ - "auth_service": "google", - "username": "YOURACCOUNT@gmail.com", - "password": "YOURPASSWORD", - "location": "SOME LOCATION", - "gmapkey": "AGMAPAPIKEY", - "max_steps": 5, - "mode": "all", - "walk": 4.16, - "debug": false, - "test": false, - "initial_transfer": 0, - "location_cache": true, - "distance_unit": "km", - "item_filter": "101,102,103,104", - "evolve_all": "NONE" -} diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example new file mode 100644 index 0000000000..8d0d8f854f --- /dev/null +++ b/configs/config.json.cluster.example @@ -0,0 +1,123 @@ +{ + "auth_service": "google", + "username": "YOUR_USERNAME", + "password": "YOUR_PASSWORD", + "location": "SOME_LOCATION", + "gmapkey": "GOOGLE_MAPS_API_KEY", + "tasks": [ + { + "type": "HandleSoftBan" + }, + { + "type": "CollectLevelUpReward" + }, + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + }, + { + "type": "TransferPokemon" + }, + { + "type": "EvolvePokemon", + "config": { + "evolve_all": "none", + "first_evolve_by": "cp", + "evolve_above_cp": 500, + "evolve_above_iv": 0.8, + "logic": "or", + "evolve_speed": 20, + "use_lucky_egg": false + } + }, + { + "type": "RecycleItems", + "config": { + "item_filter": { + "Pokeball": { "keep" : 100 }, + "Potion": { "keep" : 10 }, + "Super Potion": { "keep" : 20 }, + "Hyper Potion": { "keep" : 30 }, + "Revive": { "keep" : 30 }, + "Razz Berry": { "keep" : 100 } + } + } + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort" + }, + { + "type": "FollowCluster", + "config": { + "radius": 50, + "lured": true + } + } + ], + "forts": { + "avoid_circles": true, + "max_circle_size": 50 + }, + "websocket_server": false, + "walk": 4.16, + "action_wait_min": 1, + "action_wait_max": 4, + "debug": false, + "test": false, + "health_record": true, + "location_cache": true, + "distance_unit": "km", + "reconnecting_timeout": 15, + "evolve_captured": "NONE", + "catch_randomize_reticle_factor": 1.0, + "catch_randomize_spin_factor": 1.0, + "catch": { + "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, + "// Example of always catching Rattata:": {}, + "// Rattata": { "always_catch" : true } + }, + "release": { + "any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}, + "// Example of always releasing Rattata:": {}, + "// Rattata": {"always_release": true}, + "// Example of keeping 3 stronger (based on CP) Pidgey:": {}, + "// Pidgey": {"keep_best_cp": 3}, + "// Example of keeping 2 stronger (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_iv": 2}, + "// Also, it is working with any": {}, + "// any": {"keep_best_iv": 3}, + "// Example of keeping the 2 strongest (based on CP) and 3 best (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_cp": 2, "keep_best_iv": 3} + }, + "vips" : { + "Any pokemon put here directly force to use Berry & Best Ball to capture, to secure the capture rate!": {}, + "any": {"catch_above_cp": 1200, "catch_above_iv": 0.9, "logic": "or" }, + "Lapras": {}, + "Moltres": {}, + "Zapdos": {}, + "Articuno": {}, + + "// S-Tier pokemons (if pokemon can be evolved into tier, list the representative)": {}, + "Mewtwo": {}, + "Dragonite": {}, + "Snorlax": {}, + "// Mew evolves to Mewtwo": {}, + "Mew": {}, + "Arcanine": {}, + "Vaporeon": {}, + "Gyarados": {}, + "Exeggutor": {}, + "Muk": {}, + "Weezing": {}, + "Flareon": {} + + } +} diff --git a/configs/config.json.example b/configs/config.json.example new file mode 100644 index 0000000000..20ef72e34e --- /dev/null +++ b/configs/config.json.example @@ -0,0 +1,131 @@ +{ + "auth_service": "google", + "username": "YOUR_USERNAME", + "password": "YOUR_PASSWORD", + "location": "SOME_LOCATION", + "gmapkey": "GOOGLE_MAPS_API_KEY", + "tasks": [ + { + "type": "HandleSoftBan" + }, + { + "type": "CollectLevelUpReward" + }, + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + }, + { + "type": "TransferPokemon" + }, + { + "type": "EvolvePokemon", + "config": { + "evolve_all": "none", + "first_evolve_by": "cp", + "evolve_above_cp": 500, + "evolve_above_iv": 0.8, + "logic": "or", + "evolve_speed": 20, + "use_lucky_egg": false + } + }, + { + "type": "RecycleItems", + "config": { + "item_filter": { + "Pokeball": { "keep" : 100 }, + "Potion": { "keep" : 10 }, + "Super Potion": { "keep" : 20 }, + "Hyper Potion": { "keep" : 30 }, + "Revive": { "keep" : 30 }, + "Razz Berry": { "keep" : 100 } + } + } + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort" + }, + { + "type": "MoveToFort", + "config": { + "lure_attraction": true, + "lure_max_distance": 2000 + } + }, + { + "type": "FollowSpiral", + "config": { + "diameter": 4, + "step_size": 70 + } + } + ], + "map_object_cache_time": 5, + "forts": { + "avoid_circles": true, + "max_circle_size": 50 + }, + "websocket_server": false, + "walk": 4.16, + "action_wait_min": 1, + "action_wait_max": 4, + "debug": false, + "test": false, + "health_record": true, + "location_cache": true, + "distance_unit": "km", + "reconnecting_timeout": 15, + "evolve_captured": "NONE", + "catch_randomize_reticle_factor": 1.0, + "catch_randomize_spin_factor": 1.0, + "catch": { + "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, + "// Example of always catching Rattata:": {}, + "// Rattata": { "always_catch" : true } + }, + "release": { + "any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}, + "// Example of always releasing Rattata:": {}, + "// Rattata": {"always_release": true}, + "// Example of keeping 3 stronger (based on CP) Pidgey:": {}, + "// Pidgey": {"keep_best_cp": 3}, + "// Example of keeping 2 stronger (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_iv": 2}, + "// Also, it is working with any": {}, + "// any": {"keep_best_iv": 3}, + "// Example of keeping the 2 strongest (based on CP) and 3 best (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_cp": 2, "keep_best_iv": 3} + }, + "vips" : { + "Any pokemon put here directly force to use Berry & Best Ball to capture, to secure the capture rate!": {}, + "any": {"catch_above_cp": 1200, "catch_above_iv": 0.9, "logic": "or" }, + "Lapras": {}, + "Moltres": {}, + "Zapdos": {}, + "Articuno": {}, + + "// S-Tier pokemons (if pokemon can be evolved into tier, list the representative)": {}, + "Mewtwo": {}, + "Dragonite": {}, + "Snorlax": {}, + "// Mew evolves to Mewtwo": {}, + "Mew": {}, + "Arcanine": {}, + "Vaporeon": {}, + "Gyarados": {}, + "Exeggutor": {}, + "Muk": {}, + "Weezing": {}, + "Flareon": {} + + } +} diff --git a/configs/config.json.map.example b/configs/config.json.map.example new file mode 100644 index 0000000000..e665d4c6da --- /dev/null +++ b/configs/config.json.map.example @@ -0,0 +1,361 @@ +{ + "auth_service": "google", + "username": "YOUR_USERNAME", + "password": "YOUR_PASSWORD", + "location": "SOME_LOCATION", + "gmapkey": "GOOGLE_MAPS_API_KEY", + "tasks": [ + { + "type": "HandleSoftBan" + }, + { + "type": "CollectLevelUpReward" + }, + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + }, + { + "type": "TransferPokemon" + }, + { + "type": "EvolvePokemon", + "config": { + "evolve_all": "NONE", + "evolve_cp_min": 300, + "evolve_speed": 20, + "use_lucky_egg": false + } + }, + { + "type": "RecycleItems", + "config": { + "item_filter": { + "Pokeball": { "keep" : 100 }, + "Potion": { "keep" : 10 }, + "Super Potion": { "keep" : 20 }, + "Hyper Potion": { "keep" : 30 }, + "Revive": { "keep" : 30 }, + "Razz Berry": { "keep" : 100 } + } + } + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort" + }, + { + "type": "MoveToMapPokemon", + "config": { + "address": "http://localhost:5000", + "max_distance": 500, + "min_time": 60, + "prioritize_vips": true, + "snipe": false, + "update_map": true, + "mode": "priority", + "catch": { + "==========Legendaries==========": 0, + "Aerodactyl": 1000, + "Snorlax": 1000, + "Articuno": 1000, + "Zapdos": 1000, + "Moltres": 1000, + "Dratini": 1000, + "Dragonair": 1000, + "Dragonite": 1000, + "Mewtwo": 1000, + "Mew": 1000, + + "==========Region Locked==========": 0, + "Farfetch'd": 1000, + "Kangaskhan": 1000, + "Mr. Mime": 1000, + "Tauros": 1000, + + "==========Very Rare==========": 0, + "Lapras": 900, + "Electabuzz": 900, + "Magmar": 900, + "Ditto": 900, + + "==========Starters==========": 0, + "Bulbasaur": 400, + "Ivysaur": 600, + "Venusaur": 1000, + + "Charmander": 400, + "Charmeleon": 600, + "Charizard": 1000, + + "Squirtle": 400, + "Wartortle": 600, + "Blastoise": 1000, + + "Pikachu": 600, + "Raichu": 1000, + + "==========Semi Rare==========": 0, + "Porygon": 200, + "Scyther": 200, + "Jynx": 200, + + "==========Uncommon==========": 0, + + "Omanyte": 150, + "Omastar": 500, + + "Seel": 300, + "Dewgong": 500, + + "Grimer": 200, + "Muk": 500, + + "Shellder": 200, + "Cloyster": 500, + + "Gastly": 200, + "Haunter": 500, + "Gengar": 1000, + + "Onix": 600, + + "Drowzee": 600, + + "Hypno": 600, + + "Vulpix": 200, + "Ninetales": 600, + + "Paras": 100, + "Parasect": 500, + + "Growlithe": 200, + "Arcanine": 700, + + "Tentacool": 200, + "Tentacruel": 500, + + "Mankey": 150, + "Primeape": 500, + + "Clefairy": 150, + "Clefable": 500, + + "Jigglypuff": 150, + "Wigglytuff": 500, + + "Venonat": 100, + "Venomoth": 500, + + "Diglett": 200, + "Dugtrio": 500, + + "Meowth": 250, + "Persian": 500, + + "Psyduck": 150, + "Golduck": 500, + + "Geodude": 100, + "Graveler": 500, + "Golem": 800, + + "Eevee": 200, + "Vaporeon": 800, + "Jolteon": 800, + "Flareon": 800, + + "Kabuto": 150, + "Kabutops": 500, + + "Magikarp": 150, + "Gyarados": 800, + + "Pinsir": 150, + + "Ponyta": 200, + "Rapidash": 500, + + "Slowpoke": 200, + "Slowbro": 500, + + "Magnemite": 250, + "Magneton": 500, + + "Krabby": 100, + "Kingler": 500, + + "Voltorb": 200, + "Electrode": 500, + + "Exeggcute": 250, + "Exeggcutor": 500, + + "Cubone": 300, + "Marowak": 800, + + "Hitmonlee": 400, + + "Hitmonchan": 400, + + "Lickitung": 500, + + "Koffing": 200, + "Weezing": 500, + + "Rhyhorn": 200, + "Rhydon": 500, + + "Chansey": 800, + + "Tangela": 300, + + "Horsea": 200, + "Seadra": 600, + + "Goldeen": 150, + "Seaking": 500, + + "Staryu": 200, + "Starmie": 800, + + + "==========T1 Evolvers==========": 0, + "Caterpie": 10, + "Metapod": 10, + "Butterfree": 500, + + "Weedle": 10, + "Kakuna": 10, + "Beedrill": 500, + + "Pidgey": 10, + "Pidgeotto": 10, + "Pidgeot": 300, + + "==========T2 Evolvers==========": 0, + "Nidoran F": 10, + "Nidorina": 10, + "Nidoqueen": 10, + + "Nidoran M": 10, + "Nidorino": 10, + "Nidoking": 10, + + "Oddish": 100, + "Gloom": 200, + "Vileplume": 600, + + "Poliwag": 200, + "Poliwhirl": 400, + "Poliwrath": 800, + + "Abra": 300, + "Kadabra": 600, + "Alakazam": 800, + + "Machop": 150, + "Machoke": 400, + "Machamp": 800, + + "Bellsprout": 100, + "Weepinbell": 400, + "Victreebel": 800, + + "==========Trash==========": 0, + + "Rattata": 10, + "Raticate": 10, + + "Spearow": 10, + "Fearow": 10, + + "Ekans": 10, + "Arbok": 10, + + "Sandshrew": 10, + "Sandslash": 10, + + "Zubat": 10, + "Golbat": 10, + + "Doduo": 10, + "Dodrio": 10 + } + } + }, + { + "type": "MoveToFort" + }, + { + "type": "FollowSpiral" + } + ], + "map_object_cache_time": 5, + "forts": { + "avoid_circles": true, + "max_circle_size": 50 + }, + "websocket_server": false, + "walk": 4.16, + "action_wait_min": 1, + "action_wait_max": 4, + "debug": false, + "test": false, + "health_record": true, + "location_cache": true, + "distance_unit": "km", + "reconnecting_timeout": 15, + "evolve_captured": "NONE", + "catch_randomize_reticle_factor": 1.0, + "catch_randomize_spin_factor": 1.0, + "catch": { + "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, + "// Example of always catching Rattata:": {}, + "// Rattata": { "always_catch" : true } + }, + "release": { + "any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}, + "// Example of always releasing Rattata:": {}, + "// Rattata": {"always_release": true}, + "// Example of keeping 3 stronger (based on CP) Pidgey:": {}, + "// Pidgey": {"keep_best_cp": 3}, + "// Example of keeping 2 stronger (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_iv": 2}, + "// Also, it is working with any": {}, + "// any": {"keep_best_iv": 3}, + "// Example of keeping the 2 strongest (based on CP) and 3 best (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_cp": 2, "keep_best_iv": 3} + }, + "vips" : { + "Any pokemon put here directly force to use Berry & Best Ball to capture, to secure the capture rate!": {}, + "any": {"catch_above_cp": 1200, "catch_above_iv": 0.9, "logic": "or" }, + "Lapras": {}, + "Moltres": {}, + "Zapdos": {}, + "Articuno": {}, + + "// S-Tier pokemons (if pokemon can be evolved into tier, list the representative)": {}, + "Mewtwo": {}, + "Dragonite": {}, + "Snorlax": {}, + "// Mew evolves to Mewtwo": {}, + "Mew": {}, + "Arcanine": {}, + "Vaporeon": {}, + "Gyarados": {}, + "Exeggutor": {}, + "Muk": {}, + "Weezing": {}, + "Flareon": {} + + } +} diff --git a/configs/config.json.path.example b/configs/config.json.path.example new file mode 100644 index 0000000000..afd1e3afeb --- /dev/null +++ b/configs/config.json.path.example @@ -0,0 +1,101 @@ +{ + "auth_service": "google", + "username": "YOUR_USERNAME", + "password": "YOUR_PASSWORD", + "location": "SOME_LOCATION", + "gmapkey": "GOOGLE_MAPS_API_KEY", + "tasks": [ + { + "type": "HandleSoftBan" + }, + { + "type": "CollectLevelUpReward" + }, + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + }, + { + "type": "TransferPokemon" + }, + { + "type": "EvolvePokemon", + "config": { + "evolve_all": "none", + "first_evolve_by": "cp", + "evolve_above_cp": 500, + "evolve_above_iv": 0.8, + "logic": "or", + "evolve_speed": 20, + "use_lucky_egg": false + } + }, + { + "type": "RecycleItems", + "config": { + "item_filter": { + "Pokeball": { "keep" : 100 }, + "Potion": { "keep" : 10 }, + "Super Potion": { "keep" : 20 }, + "Hyper Potion": { "keep" : 30 }, + "Revive": { "keep" : 30 }, + "Razz Berry": { "keep" : 100 } + } + } + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort" + }, + { + "type": "FollowPath", + "config": { + "path_mode": "loop", + "path_file": "configs/path.example.json" + } + } + ], + "map_object_cache_time": 5, + "forts": { + "avoid_circles": true, + "max_circle_size": 50 + }, + "websocket_server": false, + "walk": 4.16, + "action_wait_min": 1, + "action_wait_max": 4, + "debug": false, + "test": false, + "health_record": true, + "location_cache": true, + "distance_unit": "km", + "reconnecting_timeout": 15, + "evolve_captured": "NONE", + "catch_randomize_reticle_factor": 1.0, + "catch_randomize_spin_factor": 1.0, + "catch": { + "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, + "// Example of always catching Rattata:": {}, + "// Rattata": { "always_catch" : true } + }, + "release": { + "any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}, + "// Example of always releasing Rattata:": {}, + "// Rattata": {"always_release": true}, + "// Example of keeping 3 stronger (based on CP) Pidgey:": {}, + "// Pidgey": {"keep_best_cp": 3}, + "// Example of keeping 2 stronger (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_iv": 2}, + "// Also, it is working with any": {}, + "// any": {"keep_best_iv": 3}, + "// Example of keeping the 2 strongest (based on CP) and 3 best (based on IV) Zubat:": {}, + "// Zubat": {"keep_best_cp": 2, "keep_best_iv": 3} + } +} diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example new file mode 100644 index 0000000000..7cad1ac066 --- /dev/null +++ b/configs/config.json.pokemon.example @@ -0,0 +1,357 @@ +{ + "auth_service": "google", + "username": "YOUR_USERNAME", + "password": "YOUR_PASSWORD", + "location": "SOME_LOCATION", + "gmapkey": "GOOGLE_MAPS_API_KEY", + "tasks": [ + { + "type": "HandleSoftBan" + }, + { + "type": "CollectLevelUpReward" + }, + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + }, + { + "type": "TransferPokemon" + }, + { + "type": "EvolvePokemon", + "config": { + "evolve_all": "none", + "first_evolve_by": "cp", + "evolve_above_cp": 500, + "evolve_above_iv": 0.8, + "logic": "or", + "evolve_speed": 20, + "use_lucky_egg": false + } + }, + { + "type": "RecycleItems", + "config": { + "item_filter": { + "Pokeball": { "keep" : 100 }, + "Potion": { "keep" : 10 }, + "Super Potion": { "keep" : 20 }, + "Hyper Potion": { "keep" : 30 }, + "Revive": { "keep" : 30 }, + "Razz Berry": { "keep" : 100 } + } + } + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort" + }, + { + "type": "MoveToFort", + "config":{ + "lure_attraction": true, + "lure_max_distance": 2000 + } + }, + { + "type": "FollowSpiral", + "config": { + "diameter": 4, + "step_size": 70 + } + } + ], + "map_object_cache_time": 5, + "forts": { + "avoid_circles": true, + "max_circle_size": 50 + }, + "websocket_server": false, + "walk": 4.16, + "action_wait_min": 1, + "action_wait_max": 4, + "debug": false, + "test": false, + "health_record": true, + "location_cache": true, + "distance_unit": "km", + "reconnecting_timeout": 15, + "evolve_captured": "NONE", + "catch_randomize_reticle_factor": 1.0, + "catch_randomize_spin_factor": 1.0, + "catch": { + "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or" }, + + "// Pokemons with example": { "always_catch": true }, + "// Gets filtered with release parameters": {}, + + "// Legendary pokemons (Goes under S-Tier)": {}, + "Lapras": { "always_catch": true }, + "Moltres": { "always_catch": true }, + "Zapdos": { "always_catch": true }, + "Articuno": { "always_catch": true }, + + "// S-Tier pokemons (if pokemon can be evolved into tier, list the representative)": {}, + "Mewtwo": { "always_catch": true }, + "Dragonite": { "always_catch": true }, + "Snorlax": { "always_catch": true }, + "// Mew evolves to Mewtwo": {}, + "Mew": { "always_catch": true }, + "Arcanine": { "always_catch": true }, + "Vaporeon": { "always_catch": true }, + "Gyarados": { "always_catch": true }, + "Exeggutor": { "always_catch": true }, + "Muk": { "always_catch": true }, + "Weezing": { "always_catch": true }, + "Flareon": { "always_catch": true }, + + "// Growlithe evolves to Arcanine": {}, + "Growlithe": { "always_catch": true }, + "// Dragonair evolves to Dragonite": {}, + "Dragonair": { "always_catch": true }, + "// Grimer evolves to Muk": {}, + "Grimer": { "always_catch": true }, + + "// Magikarp evolves to Gyarados": {}, + "Magikarp": { "always_catch": true }, + "// Exeggcute evolves to Exeggutor": {}, + "Exeggcute": { "always_catch": true }, + "// Eevee evolves to many versions, like Vaporeon, Flareon": {}, + "Eevee": { "always_catch": true }, + + "// A-Tier pokemons": {}, + "Slowbro": { "always_catch": true }, + "Victreebel": { "always_catch": true }, + "Machamp": { "always_catch": true }, + "Poliwrath": { "always_catch": true }, + "Clefable": { "always_catch": true }, + "Nidoking": { "always_catch": true }, + "Venusaur": { "always_catch": true }, + "Charizard": { "always_catch": true }, + "Golduck": { "always_catch": true }, + "Nidoqueen": { "always_catch": true }, + "Vileplume": { "always_catch": true }, + "Blastoise": { "always_catch": true }, + "Omastar": { "always_catch": true }, + "Aerodactyl": { "always_catch": true }, + "Golem": { "always_catch": true }, + "Wigglytuff": { "always_catch": true }, + "Dewgong": { "always_catch": true }, + "Ninetales": { "always_catch": true }, + "Magmar": { "always_catch": true }, + "Kabutops": { "always_catch": true }, + "Electabuzz": { "always_catch": true }, + "Starmie": { "always_catch": true }, + "Jolteon": { "always_catch": true }, + "Rapidash": { "always_catch": true }, + "Pinsir": { "always_catch": true }, + "Scyther": { "always_catch": true }, + "Tentacruel": { "always_catch": true }, + "Gengar": { "always_catch": true }, + "Hypno": { "always_catch": true }, + "Pidgeot": { "always_catch": true }, + "Rhydon": { "always_catch": true }, + "Seaking": { "always_catch": true }, + "Kangaskhan": { "always_catch": true } + }, + "release": { + "any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or" }, + + "// Legendary pokemons (Goes under S-Tier)": {}, + "Lapras": { "release_below_cp": 1041, "release_below_iv": 0.8, "logic": "and" }, + "Moltres": { "release_below_cp": 1132, "release_below_iv": 0.8, "logic": "and" }, + "Zapdos": { "release_below_cp": 1087, "release_below_iv": 0.8, "logic": "and" }, + "Articuno": { "release_below_cp": 1039, "release_below_iv": 0.8, "logic": "and" }, + + "// S-Tier pokemons (if pokemon can be evolved into tier, list the representative)": {}, + "Mewtwo": { "release_below_cp": 1447, "release_below_iv": 0.8, "logic": "and"}, + "Dragonite": { "release_below_cp": 1221, "release_below_iv": 0.8, "logic": "and" }, + "Snorlax": { "release_below_cp": 1087, "release_below_iv": 0.8, "logic": "and" }, + "// Mew evolves to Mewtwo": {}, + "Mew": { "release_below_cp": 1152, "release_below_iv": 0.8, "logic": "and" }, + "Arcanine": { "release_below_cp": 1041, "release_below_iv": 0.8, "logic": "and" }, + "Vaporeon": { "release_below_cp": 984, "release_below_iv": 0.8, "logic": "and" }, + "Gyarados": { "release_below_cp": 938, "release_below_iv": 0.8, "logic": "and" }, + "Exeggutor": { "release_below_cp": 1032, "release_below_iv": 0.8, "logic": "and" }, + "Muk": { "release_below_cp": 909, "release_below_iv": 0.8, "logic": "and" }, + "Weezing": { "release_below_cp": 784, "release_below_iv": 0.8, "logic": "and" }, + "Flareon": { "release_below_cp": 924, "release_below_iv": 0.8, "logic": "and" }, + + "// Growlithe evolves to Arcanine": {}, + "Growlithe": { "release_below_cp": 465, "release_below_iv": 0.8, "logic": "and" }, + "// Dragonair evolves to Dragonite": {}, + "Dragonair": { "release_below_cp": 609, "release_below_iv": 0.8, "logic": "and" }, + "// Grimer evolves to Muk": {}, + "Grimer": { "release_below_cp": 448, "release_below_iv": 0.8, "logic": "and" }, + "// Magikarp evolves to Gyarados": {}, + "Magikarp": { "release_below_cp": 91, "release_below_iv": 0.8, "logic": "and" }, + "// Exeggcute evolves to Exeggutor": {}, + "Exeggcute": { "release_below_cp": 384, "release_below_iv": 0.8, "logic": "and" }, + "// Eevee evolves to many versions, like Vaporeon, Flareon": {}, + "Eevee": { "release_below_cp": 376, "release_below_iv": 0.8, "logic": "and" }, + + "// A-Tier pokemons": {}, + "Slowbro": { "release_below_cp": 907, "release_below_iv": 0.8, "logic": "and" }, + "Victreebel": { "release_below_cp": 883, "release_below_iv": 0.8, "logic": "and" }, + "Machamp": { "release_below_cp": 907, "release_below_iv": 0.8, "logic": "and" }, + "Poliwrath": { "release_below_cp": 876, "release_below_iv": 0.8, "logic": "and" }, + "Clefable": { "release_below_cp": 837, "release_below_iv": 0.8, "logic": "and" }, + "Nidoking": { "release_below_cp": 864, "release_below_iv": 0.8, "logic": "and" }, + "Venusaur": { "release_below_cp": 902, "release_below_iv": 0.8, "logic": "and" }, + "Charizard": { "release_below_cp": 909, "release_below_iv": 0.8, "logic": "and" }, + "Golduck": { "release_below_cp": 832, "release_below_iv": 0.8, "logic": "and" }, + "Nidoqueen": { "release_below_cp": 868, "release_below_iv": 0.8, "logic": "and" }, + "Vileplume": { "release_below_cp": 871, "release_below_iv": 0.8, "logic": "and" }, + "Blastoise": { "release_below_cp": 888, "release_below_iv": 0.8, "logic": "and" }, + "Omastar": { "release_below_cp": 780, "release_below_iv": 0.8, "logic": "and" }, + "Aerodactyl": { "release_below_cp": 756, "release_below_iv": 0.8, "logic": "and" }, + "Golem": { "release_below_cp": 804, "release_below_iv": 0.8, "logic": "and" }, + "Wigglytuff": { "release_below_cp": 760, "release_below_iv": 0.8, "logic": "and" }, + "Dewgong": { "release_below_cp": 748, "release_below_iv": 0.8, "logic": "and" }, + "Ninetales": { "release_below_cp": 763, "release_below_iv": 0.8, "logic": "and" }, + "Magmar": { "release_below_cp": 792, "release_below_iv": 0.8, "logic": "and" }, + "Kabutops": { "release_below_cp": 744, "release_below_iv": 0.8, "logic": "and" }, + "Electabuzz": { "release_below_cp": 739, "release_below_iv": 0.8, "logic": "and" }, + "Starmie": { "release_below_cp": 763, "release_below_iv": 0.8, "logic": "and" }, + "Jolteon": { "release_below_cp": 746, "release_below_iv": 0.8, "logic": "and" }, + "Rapidash": { "release_below_cp": 768, "release_below_iv": 0.8, "logic": "and" }, + "Pinsir": { "release_below_cp": 741, "release_below_iv": 0.8, "logic": "and" }, + "Scyther": { "release_below_cp": 724, "release_below_iv": 0.8, "logic": "and" }, + "Tentacruel": { "release_below_cp": 775, "release_below_iv": 0.8, "logic": "and" }, + "Gengar": { "release_below_cp": 724, "release_below_iv": 0.8, "logic": "and" }, + "Hypno": { "release_below_cp": 763, "release_below_iv": 0.8, "logic": "and" }, + "Pidgeot": { "release_below_cp": 729, "release_below_iv": 0.8, "logic": "and" }, + "Rhydon": { "release_below_cp": 782, "release_below_iv": 0.8, "logic": "and" }, + "Seaking": { "release_below_cp": 712, "release_below_iv": 0.8, "logic": "and" }, + "Kangaskhan": { "release_below_cp": 712, "release_below_iv": 0.8, "logic": "and" }, + + "// Koffing evolves to Weezing (A-Tier)": {}, + "Koffing": { "release_below_cp": 403, "release_below_iv": 0.8, "logic": "and" }, + + "// Below is B-tier and lower pokemons": {}, + "Caterpie": { "release_below_cp": 156, "release_below_iv": 0.8, "logic": "and" }, + "Weedle": { "release_below_cp": 156, "release_below_iv": 0.8, "logic": "and" }, + "Diglett": { "release_below_cp": 158, "release_below_iv": 0.8, "logic": "and" }, + "Metapod": { "release_below_cp": 168, "release_below_iv": 0.8, "logic": "and" }, + "Kakuna": { "release_below_cp": 170, "release_below_iv": 0.8, "logic": "and" }, + "Rattata": { "release_below_cp": 204, "release_below_iv": 0.8, "logic": "and" }, + "Abra": { "release_below_cp": 208, "release_below_iv": 0.8, "logic": "and" }, + "Zubat": { "release_below_cp": 225, "release_below_iv": 0.8, "logic": "and" }, + "Chansey": { "release_below_cp": 235, "release_below_iv": 0.8, "logic": "and" }, + "Pidgey": { "release_below_cp": 237, "release_below_iv": 0.8, "logic": "and" }, + "Spearow": { "release_below_cp": 240, "release_below_iv": 0.8, "logic": "and" }, + "Meowth": { "release_below_cp": 264, "release_below_iv": 0.8, "logic": "and" }, + "Krabby": { "release_below_cp": 276, "release_below_iv": 0.8, "logic": "and" }, + "Sandshrew": { "release_below_cp": 278, "release_below_iv": 0.8, "logic": "and" }, + "Poliwag": { "release_below_cp": 278, "release_below_iv": 0.8, "logic": "and" }, + "Horsea": { "release_below_cp": 278, "release_below_iv": 0.8, "logic": "and" }, + "Gastly": { "release_below_cp": 280, "release_below_iv": 0.8, "logic": "and" }, + "Ekans": { "release_below_cp": 288, "release_below_iv": 0.8, "logic": "and" }, + "Shellder": { "release_below_cp": 288, "release_below_iv": 0.8, "logic": "and" }, + "Vulpix": { "release_below_cp": 290, "release_below_iv": 0.8, "logic": "and" }, + "Voltorb": { "release_below_cp": 292, "release_below_iv": 0.8, "logic": "and" }, + "Geodude": { "release_below_cp": 297, "release_below_iv": 0.8, "logic": "and" }, + "Doduo": { "release_below_cp": 297, "release_below_iv": 0.8, "logic": "and" }, + "Onix": { "release_below_cp": 300, "release_below_iv": 0.8, "logic": "and" }, + "Mankey": { "release_below_cp": 307, "release_below_iv": 0.8, "logic": "and" }, + "Pikachu": { "release_below_cp": 309, "release_below_iv": 0.8, "logic": "and" }, + "Magnemite": { "release_below_cp": 312, "release_below_iv": 0.8, "logic": "and" }, + "Tentacool": { "release_below_cp": 316, "release_below_iv": 0.8, "logic": "and" }, + "Paras": { "release_below_cp": 319, "release_below_iv": 0.8, "logic": "and" }, + "Jigglypuff": { "release_below_cp": 321, "release_below_iv": 0.8, "logic": "and" }, + "Ditto": { "release_below_cp": 321, "release_below_iv": 0.8, "logic": "and" }, + "Staryu": { "release_below_cp": 326, "release_below_iv": 0.8, "logic": "and" }, + "Charmander": { "release_below_cp": 333, "release_below_iv": 0.8, "logic": "and" }, + "Goldeen": { "release_below_cp": 336, "release_below_iv": 0.8, "logic": "and" }, + "Squirtle": { "release_below_cp": 352, "release_below_iv": 0.8, "logic": "and" }, + "Cubone": { "release_below_cp": 352, "release_below_iv": 0.8, "logic": "and" }, + "Venonat": { "release_below_cp": 360, "release_below_iv": 0.8, "logic": "and" }, + "Bulbasaur": { "release_below_cp": 374, "release_below_iv": 0.8, "logic": "and" }, + "Drowzee": { "release_below_cp": 374, "release_below_iv": 0.8, "logic": "and" }, + "Machop": { "release_below_cp": 381, "release_below_iv": 0.8, "logic": "and" }, + "Psyduck": { "release_below_cp": 386, "release_below_iv": 0.8, "logic": "and" }, + "Seel": { "release_below_cp": 386, "release_below_iv": 0.8, "logic": "and" }, + "Kabuto": { "release_below_cp": 386, "release_below_iv": 0.8, "logic": "and" }, + "Bellsprout": { "release_below_cp": 391, "release_below_iv": 0.8, "logic": "and" }, + "Omanyte": { "release_below_cp": 391, "release_below_iv": 0.8, "logic": "and" }, + "Kadabra": { "release_below_cp": 396, "release_below_iv": 0.8, "logic": "and" }, + "Oddish": { "release_below_cp": 400, "release_below_iv": 0.8, "logic": "and" }, + "Dugtrio": { "release_below_cp": 408, "release_below_iv": 0.8, "logic": "and" }, + "Rhyhorn": { "release_below_cp": 412, "release_below_iv": 0.8, "logic": "and" }, + "Clefairy": { "release_below_cp": 420, "release_below_iv": 0.8, "logic": "and" }, + "Slowpoke": { "release_below_cp": 424, "release_below_iv": 0.8, "logic": "and" }, + "Pidgeotto": { "release_below_cp": 427, "release_below_iv": 0.8, "logic": "and" }, + "Farfetch'd": { "release_below_cp": 441, "release_below_iv": 0.8, "logic": "and" }, + "Poliwhirl": { "release_below_cp": 468, "release_below_iv": 0.8, "logic": "and" }, + "Nidorino": { "release_below_cp": 480, "release_below_iv": 0.8, "logic": "and" }, + "Haunter": { "release_below_cp": 482, "release_below_iv": 0.8, "logic": "and" }, + "Nidorina": { "release_below_cp": 489, "release_below_iv": 0.8, "logic": "and" }, + "Graveler": { "release_below_cp": 501, "release_below_iv": 0.8, "logic": "and" }, + "Beedrill": { "release_below_cp": 504, "release_below_iv": 0.8, "logic": "and" }, + "Raticate": { "release_below_cp": 504, "release_below_iv": 0.8, "logic": "and" }, + "Butterfree": { "release_below_cp": 508, "release_below_iv": 0.8, "logic": "and" }, + "Hitmonlee": { "release_below_cp": 520, "release_below_iv": 0.8, "logic": "and" }, + "Ponyta": { "release_below_cp": 530, "release_below_iv": 0.8, "logic": "and" }, + "Hitmonchan": { "release_below_cp": 530, "release_below_iv": 0.8, "logic": "and" }, + "Charmeleon": { "release_below_cp": 544, "release_below_iv": 0.8, "logic": "and" }, + "Wartortle": { "release_below_cp": 552, "release_below_iv": 0.8, "logic": "and" }, + "Persian": { "release_below_cp": 568, "release_below_iv": 0.8, "logic": "and" }, + "Lickitung": { "release_below_cp": 568, "release_below_iv": 0.8, "logic": "and" }, + "Ivysaur": { "release_below_cp": 571, "release_below_iv": 0.8, "logic": "and" }, + "Electrode": { "release_below_cp": 576, "release_below_iv": 0.8, "logic": "and" }, + "Marowak": { "release_below_cp": 578, "release_below_iv": 0.8, "logic": "and" }, + "Gloom": { "release_below_cp": 590, "release_below_iv": 0.8, "logic": "and" }, + "Porygon": { "release_below_cp": 590, "release_below_iv": 0.8, "logic": "and" }, + "Seadra": { "release_below_cp": 597, "release_below_iv": 0.8, "logic": "and" }, + "Jynx": { "release_below_cp": 600, "release_below_iv": 0.8, "logic": "and" }, + "Weepinbell": { "release_below_cp": 602, "release_below_iv": 0.8, "logic": "and" }, + "Tangela": { "release_below_cp": 607, "release_below_iv": 0.8, "logic": "and" }, + "Fearow": { "release_below_cp": 609, "release_below_iv": 0.8, "logic": "and" }, + "Parasect": { "release_below_cp": 609, "release_below_iv": 0.8, "logic": "and" }, + "Machoke": { "release_below_cp": 614, "release_below_iv": 0.8, "logic": "and" }, + "Arbok": { "release_below_cp": 616, "release_below_iv": 0.8, "logic": "and" }, + "Sandslash": { "release_below_cp": 631, "release_below_iv": 0.8, "logic": "and" }, + "Alakazam": { "release_below_cp": 633, "release_below_iv": 0.8, "logic": "and" }, + "Kingler": { "release_below_cp": 636, "release_below_iv": 0.8, "logic": "and" }, + "Dodrio": { "release_below_cp": 640, "release_below_iv": 0.8, "logic": "and" }, + "Tauros": { "release_below_cp": 643, "release_below_iv": 0.8, "logic": "and" }, + "Primeape": { "release_below_cp": 650, "release_below_iv": 0.8, "logic": "and" }, + "Magneton": { "release_below_cp": 657, "release_below_iv": 0.8, "logic": "and" }, + "Venomoth": { "release_below_cp": 660, "release_below_iv": 0.8, "logic": "and" }, + "Golbat": { "release_below_cp": 672, "release_below_iv": 0.8, "logic": "and" }, + "Raichu": { "release_below_cp": 708, "release_below_iv": 0.8, "logic": "and" }, + "Cloyster": { "release_below_cp": 717, "release_below_iv": 0.8, "logic": "and"}, + "Mr. Mime": { "release_below_cp": 650, "release_below_iv": 0.8, "logic": "and" } + }, + "vips" : { + "Any pokemon put here directly force to use Berry & Best Ball to capture, to secure the capture rate!": {}, + "any": {"catch_above_cp": 1200, "catch_above_iv": 0.9, "logic": "or" }, + "Lapras": {}, + "Moltres": {}, + "Zapdos": {}, + "Articuno": {}, + + "// S-Tier pokemons (if pokemon can be evolved into tier, list the representative)": {}, + "Mewtwo": {}, + "Dragonite": {}, + "Snorlax": {}, + "// Mew evolves to Mewtwo": {}, + "Mew": {}, + "Arcanine": {}, + "Vaporeon": {}, + "Gyarados": {}, + "Exeggutor": {}, + "Muk": {}, + "Weezing": {}, + "Flareon": {} + + } +} diff --git a/configs/path.example.json b/configs/path.example.json new file mode 100644 index 0000000000..4936880587 --- /dev/null +++ b/configs/path.example.json @@ -0,0 +1,6 @@ +[ + {"location": "32.087504, 34.806118"}, + {"location": "Bialik 150, Ramat Gan"}, + {"location": "Ayalon Highway, Ramat Gan"}, + {"location": "32.091280, 34.795261"} +] diff --git a/data/pokemon.json b/data/pokemon.json index 795343c572..a227106841 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -1 +1 @@ -[{"Number":"001","Name":"Bulbasaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Tackle","Vine Whip"],"Weight":"6.9 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"002","Name":"Ivysaur"},{"Number":"003","Name":"Venusaur"}]},{"Number":"002","Name":"Ivysaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"13.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"003","Name":"Venusaur"}]},{"Number":"003","Name":"Venusaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"100.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"},{"Number":"002","Name":"Ivysaur"}]},{"Number":"004","Name":"Charmander","Classification":"Lizard Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Scratch"],"Weight":"8.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"005","Name":"Charmeleon"},{"Number":"006","Name":"Charizard"}]},{"Number":"005","Name":"Charmeleon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"19.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"}],"Next Evolution Requirements":{"Amount":100,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"006","Name":"Charizard"}]},{"Number":"006","Name":"Charizard","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Ember","Wing Attack"],"Weight":"90.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"},{"Number":"005","Name":"Charmeleon"}]},{"Number":"007","Name":"Squirtle","Classification":"Tiny Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Tackle","Bubble"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"008","Name":"Wartortle"},{"Number":"009","Name":"Blastoise"}]},{"Number":"008","Name":"Wartortle","Classification":"Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"22.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"}],"Next Evolution Requirements":{"Amount":100,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"009","Name":"Blastoise"}]},{"Number":"009","Name":"Blastoise","Classification":"Shellfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"85.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"},{"Number":"008","Name":"Wartortle"}]},{"Number":"010","Name":"Caterpie","Classification":"Worm Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"2.9 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"011","Name":"Metapod"},{"Number":"012","Name":"Butterfree"}]},{"Number":"011","Name":"Metapod","Classification":"Cocoon Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"9.9 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"}],"Next Evolution Requirements":{"Amount":50,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"012","Name":"Butterfree"}]},{"Number":"012","Name":"Butterfree","Classification":"Butterfly Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"32.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"},{"Number":"011","Name":"Metapod"}]},{"Number":"013","Name":"Weedle","Classification":"Hairy Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Sting"],"Weight":"3.2 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"014","Name":"Kakuna"},{"Number":"015","Name":"Beedrill"}]},{"Number":"014","Name":"Kakuna","Classification":"Cocoon Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Posion Sting"],"Weight":"10.0 kg","Height":"0.6 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"}],"Next Evolution Requirements":{"Amount":50,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"015","Name":"Beedrill"}]},{"Number":"015","Name":"Beedrill","Classification":"Poison Bee Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Jab"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"},{"Number":"014","Name":"Kakuna"}]},{"Number":"016","Name":"Pidgey","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"1.8 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"017","Name":"Pidgeotto"},{"Number":"018","Name":"Pidgeot"}]},{"Number":"017","Name":"Pidgeotto","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"30.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"}],"Next Evolution Requirements":{"Amount":50,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"018","Name":"Pidgeot"}]},{"Number":"018","Name":"Pidgeot","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Hurricane"],"Weight":"39.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"},{"Number":"017","Name":"Pidgeotto"}]},{"Number":"019","Name":"Rattata","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Body Slam","Dig","Hyper Fang"],"Weight":"3.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Rattata candies"},"Next evolution(s)":[{"Number":"020","Name":"Raticate"}]},{"Number":"020","Name":"Raticate","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Quick Attack"],"Special Attack(s)":["Dig","Hyper Beam","Hyper Fang"],"Weight":"18.5 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"019","Name":"Rattata"}]},{"Number":"021","Name":"Spearow","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"2.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Spearow candies"},"Next evolution(s)":[{"Number":"022","Name":"Fearow"}]},{"Number":"022","Name":"Fearow","Classification":"Beak Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Steel Wing"],"Weight":"38.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"021","Name":"Spearow"}]},{"Number":"023","Name":"Ekans","Classification":"Snake Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Sting"],"Weight":"6.9 kg","Height":"2.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ekans candies"},"Next evolution(s)":[{"Number":"024","Name":"Arbok"}]},{"Number":"024","Name":"Arbok","Classification":"Cobra Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Bite"],"Weight":"65.0 kg","Height":"3.5 m","Previous evolution(s)":[{"Number":"023","Name":"Ekans"}]},{"Number":"025","Name":"Pikachu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Quick Attack","Thunder Shock"],"Weight":"6.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Pikachu candies"},"Next evolution(s)":[{"Number":"026","Name":"Raichu"}]},{"Number":"026","Name":"Raichu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock","Spark"],"Weight":"30.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"025","Name":"Pikachu"}]},{"Number":"027","Name":"Sandshrew","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"12.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Sandshrew candies"},"Next evolution(s)":[{"Number":"028","Name":"Sandslash"}]},{"Number":"028","Name":"Sandslash","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"027","Name":"Sandshrew"}]},{"Number":"029","Name":"Nidoran F","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"7.0 kg","Height":"0.4 m","Next evolution(s)":[{"Number":"030","Name":"Nidorina"},{"Number":"031","Name":"Nidoqueen"}]},{"Number":"030","Name":"Nidorina","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"20.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"031","Name":"Nidoqueen"}]},{"Number":"031","Name":"Nidoqueen","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"},{"Number":"030","Name":"Nidorina"}]},{"Number":"032","Name":"Nidoran M","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Peck","Poison Sting"],"Weight":"9.0 kg","Height":"0.5 m","Next evolution(s)":[{"Number":"033","Name":"Nidorino"},{"Number":"034","Name":"Nidoking"}]},{"Number":"033","Name":"Nidorino","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"19.5 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"}],"Next Evolution Requirements":{"Amount":100,"Name":"NidoranM candies"},"Next evolution(s)":[{"Number":"034","Name":"Nidoking"}]},{"Number":"034","Name":"Nidoking","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Fury Cutter","Poison Jab"],"Weight":"62.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"},{"Number":"033","Name":"Nidorino"}]},{"Number":"035","Name":"Clefairy","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"7.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Clefairy candies"},"Next evolution(s)":[{"Number":"036","Name":"Clefable"}]},{"Number":"036","Name":"Clefable","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"40.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"035","Name":"Clefairy"}]},{"Number":"037","Name":"Vulpix","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"9.9 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Vulpi"},"Next evolution(s)":[{"Number":"038","Name":"Ninetales"}]},{"Number":"038","Name":"Ninetales","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"19.9 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"037","Name":"Vulpix"}]},{"Number":"039","Name":"Jigglypuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"5.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Jigglypuff candies"},"Next evolution(s)":[{"Number":"039","Name":"Jigglypuff"}]},{"Number":"040","Name":"Wigglytuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"12.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"040","Name":"Wigglytuff"}]},{"Number":"041","Name":"Zubat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Quick Attack"],"Weight":"7.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Zubat candies"},"Next evolution(s)":[{"Number":"042","Name":"Golbat"}]},{"Number":"042","Name":"Golbat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Wing Attack"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"041","Name":"Zubat"}]},{"Number":"043","Name":"Oddish","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"5.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"044","Name":"Gloom"},{"Number":"045","Name":"Vileplume"}]},{"Number":"044","Name":"Gloom","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"8.6 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"}],"Next Evolution Requirements":{"Amount":100,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"045","Name":"Vileplume"}]},{"Number":"045","Name":"Vileplume","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid",""],"Weight":"18.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"},{"Number":"044","Name":"Gloom"}]},{"Number":"046","Name":"Paras","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Scratch"],"Weight":"5.4 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Paras candies"},"Next evolution(s)":[{"Number":"047","Name":"Parasect"}]},{"Number":"047","Name":"Parasect","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Fury Cutter"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"046","Name":"Paras"}]},{"Number":"048","Name":"Venonat","Classification":"Insect Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Venonat candies"},"Next evolution(s)":[{"Number":"049","Name":"Venomoth"}]},{"Number":"049","Name":"Venomoth","Classification":"Poison Moth Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"12.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"048","Name":"Venonat"}]},{"Number":"050","Name":"Diglett","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"0.8 kg","Height":"0.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Diglett candies"},"Next evolution(s)":[{"Number":"051","Name":"Dugtrio"}]},{"Number":"051","Name":"Dugtrio","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Sucker Punch"],"Weight":"33.3 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"050","Name":"Diglett"}]},{"Number":"052","Name":"Meowth","Classification":"Scratch Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Scratch"],"Weight":"4.2 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Meowth candies"},"Next evolution(s)":[{"Number":"053","Name":"Persian"}]},{"Number":"053","Name":"Persian","Classification":"Classy Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Scratch"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"052","Name":"Meowth"}]},{"Number":"054","Name":"Psyduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun","Zen Headbutt"],"Weight":"19.6 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Psyduck candies"},"Next evolution(s)":[{"Number":"055","Name":"Golduck"}]},{"Number":"055","Name":"Golduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"76.6 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"054","Name":"Psyduck"}]},{"Number":"056","Name":"Mankey","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Scratch"],"Weight":"28.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Mankey candies"},"Next evolution(s)":[{"Number":"057","Name":"Primeape"}]},{"Number":"057","Name":"Primeape","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"056","Name":"Mankey"}]},{"Number":"058","Name":"Growlithe","Classification":"Puppy Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Ember"],"Weight":"19.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":50,"Name":"Growlithe candies"},"Next evolution(s)":[{"Number":"059","Name":"Arcanine"}]},{"Number":"059","Name":"Arcanine","Classification":"Legendary Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Fire Fang"],"Weight":"155.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"058","Name":"Growlithe"}]},{"Number":"060","Name":"Poliwag","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"12.4 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"061","Name":"Poliwhirl"},{"Number":"062","Name":"Poliwrath"}]},{"Number":"061","Name":"Poliwhirl","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"20.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"}],"Next Evolution Requirements":{"Amount":100,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"062","Name":"Poliwrath"}]},{"Number":"062","Name":"Poliwrath","Classification":"Tadpole Pokemon","Type I":["Water"],"Type II":["Fighting"],"Weaknesses":["Electric","Grass","Flying","Psychic","Fairy"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"54.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"},{"Number":"061","Name":"Poliwhirl"}]},{"Number":"063","Name":"Abra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Zen Headbutt",""],"Weight":"19.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":25,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"064","Name":"Kadabra"},{"Number":"065","Name":"Alakazam"}]},{"Number":"064","Name":"Kadabra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"56.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"}],"Next Evolution Requirements":{"Amount":100,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"065","Name":"Alakazam"}]},{"Number":"065","Name":"Alakazam","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"48.0 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"},{"Number":"064","Name":"Kadabra"}]},{"Number":"066","Name":"Machop","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"19.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"067","Name":"Machoke"},{"Number":"068","Name":"Machamp"}]},{"Number":"067","Name":"Machoke","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"70.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"}],"Next Evolution Requirements":{"Amount":100,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"068","Name":"Machamp"}]},{"Number":"068","Name":"Machamp","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Karate Chop"],"Weight":"130.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"},{"Number":"067","Name":"Machoke"}]},{"Number":"069","Name":"Bellsprout","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Vine Whip"],"Weight":"4.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"070","Name":"Weepinbell"},{"Number":"071","Name":"Victreebel"}]},{"Number":"070","Name":"Weepinbell","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"6.4 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"071","Name":"Victreebel"}]},{"Number":"071","Name":"Victreebel","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"15.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"},{"Number":"070","Name":"Weepinbell"}]},{"Number":"072","Name":"Tentacool","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Bubble","Poison Sting"],"Weight":"45.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Tentacool candies"},"Next evolution(s)":[{"Number":"073","Name":"Tentacruel"}]},{"Number":"073","Name":"Tentacruel","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Jab"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"072","Name":"Tentacool"}]},{"Number":"074","Name":"Geodude","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"20.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"075","Name":"Graveler"},{"Number":"076","Name":"Golem"}]},{"Number":"075","Name":"Graveler","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"105.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"}],"Next Evolution Requirements":{"Amount":100,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"076","Name":"Golem"}]},{"Number":"076","Name":"Golem","Classification":"Megaton Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"300.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"},{"Number":"075","Name":"Graveler"}]},{"Number":"077","Name":"Ponyta","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Tackle"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ponyta candies"},"Next evolution(s)":[{"Number":"078","Name":"Rapidash"}]},{"Number":"078","Name":"Rapidash","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Low Kick"],"Weight":"95.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"077","Name":"Ponyta"}]},{"Number":"079","Name":"Slowpoke","Classification":"Dopey Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"36.0 kg","Height":"1.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Slowpoke candies"},"Next evolution(s)":[{"Number":"080","Name":"Slowbro"}]},{"Number":"080","Name":"Slowbro","Classification":"Hermit Crab Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"78.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"079","Name":"Slowpoke"}]},{"Number":"081","Name":"Magnemite","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"6.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Magnemite candies"},"Next evolution(s)":[{"Number":"082","Name":"Magneton"}]},{"Number":"082","Name":"Magneton","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"60.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"081","Name":"Magnemite"}]},{"Number":"083","Name":"Farfetch'd","Classification":"Wild Duck Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"15.0 kg","Height":"0.8 m"},{"Number":"084","Name":"Doduo","Classification":"Twin Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"39.2 kg","Height":"1.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Doduo candies"},"Next evolution(s)":[{"Number":"085","Name":"Dodrio"}]},{"Number":"085","Name":"Dodrio","Classification":"Triple Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Feint Attack","Steel Wing"],"Weight":"85.2 kg","Height":"1.8 m","Previous evolution(s)":[{"Number":"084","Name":"Doduo"}]},{"Number":"086","Name":"Seel","Classification":"Sea Lion Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Water Gun"],"Weight":"90.0 kg","Height":"1.1 m","Next Evolution Requirements":{"Amount":50,"Name":"Seel candies"},"Next evolution(s)":[{"Number":"087","Name":"Dewgong"}]},{"Number":"087","Name":"Dewgong","Classification":"Sea Lion Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"120.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"086","Name":"Seel"}]},{"Number":"088","Name":"Grimer","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Mud Slap"],"Weight":"30.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Grimer candies"},"Next evolution(s)":[{"Number":"089","Name":"Muk"}]},{"Number":"089","Name":"Muk","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Poison Jab",""],"Weight":"30.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"088","Name":"Grimer"}]},{"Number":"090","Name":"Shellder","Classification":"Bivalve Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Tackle"],"Weight":"4.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Shellder candies"},"Next evolution(s)":[{"Number":"091","Name":"Cloyster"}]},{"Number":"091","Name":"Cloyster","Classification":"Bivalve Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"132.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"090","Name":"Shellder"}]},{"Number":"092","Name":"Gastly","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Sucker Punch"],"Weight":"0.1 kg","Height":"1.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"093","Name":"Haunter"},{"Number":"094","Name":"Gengar"}]},{"Number":"093","Name":"Haunter","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Shadow Claw"],"Weight":"0.1 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"}],"Next Evolution Requirements":{"Amount":100,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"094","Name":"Gengar"}]},{"Number":"094","Name":"Gengar","Classification":"Shadow Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Shadow Claw","Sucker Punch"],"Weight":"40.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"},{"Number":"093","Name":"Haunter"}]},{"Number":"095","Name":"Onix","Classification":"Rock Snake Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"210.0 kg","Height":"8.8 m"},{"Number":"096","Name":"Drowzee","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Pound"],"Weight":"32.4 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Drowzee candies"},"Next evolution(s)":[{"Number":"097","Name":"Hypno"}]},{"Number":"097","Name":"Hypno","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"75.6 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"096","Name":"Drowzee"}]},{"Number":"098","Name":"Krabby","Classification":"River Crab Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Krabby candies"},"Next evolution(s)":[{"Number":"099","Name":"Kingler"}]},{"Number":"099","Name":"Kingler","Classification":"Pincer Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"098","Name":"Krabby"}]},{"Number":"100","Name":"Voltorb","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark","Tackle"],"Weight":"10.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Voltorb candies"},"Next evolution(s)":[{"Number":"101","Name":"Electrode"}]},{"Number":"101","Name":"Electrode","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark",""],"Weight":"66.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"100","Name":"Voltorb"}]},{"Number":"102","Name":"Exeggcute","Classification":"Egg Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion",""],"Weight":"2.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"E"},"Next evolution(s)":[{"Number":"103","Name":"Exeggutor"}]},{"Number":"103","Name":"Exeggutor","Classification":"Coconut Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"120.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"102","Name":"Exeggcute"}]},{"Number":"104","Name":"Cubone","Classification":"Lonely Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Cubone candies"},"Next evolution(s)":[{"Number":"105","Name":"Marowak"}]},{"Number":"105","Name":"Marowak","Classification":"Bone Keeper Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"45.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"104","Name":"Cubone"}]},{"Number":"106","Name":"Hitmonlee","Classification":"Kicking Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Low Kick","Rock Smash"],"Weight":"49.8 kg","Height":"1.5 m","Next evolution(s)":[{"Number":"107","Name":"Hitmonchan"}]},{"Number":"107","Name":"Hitmonchan","Classification":"Punching Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Rock Smash"],"Weight":"50.2 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"106","Name":"Hitmonlee"}]},{"Number":"108","Name":"Lickitung","Classification":"Licking Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"65.5 kg","Height":"1.2 m"},{"Number":"109","Name":"Koffing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"1.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Koffing candies"},"Next evolution(s)":[{"Number":"110","Name":"Weezing"}]},{"Number":"110","Name":"Weezing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"9.5 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"109","Name":"Koffing"}]},{"Number":"111","Name":"Rhyhorn","Classification":"Spikes Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"115.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Rhyhorn candies"},"Next evolution(s)":[{"Number":"112","Name":"Rhydon"}]},{"Number":"112","Name":"Rhydon","Classification":"Drill Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"120.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"111","Name":"Rhyhorn"}]},{"Number":"113","Name":"Chansey","Classification":"Egg Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"34.6 kg","Height":"1.1 m"},{"Number":"114","Name":"Tangela","Classification":"Vine Pokemon","Type I":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug"],"Fast Attack(s)":["Vine Whip",""],"Weight":"35.0 kg","Height":"1.0 m"},{"Number":"115","Name":"Kangaskhan","Classification":"Parent Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Low Kick",""],"Weight":"80.0 kg","Height":"2.2 m"},{"Number":"116","Name":"Horsea","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Water Gun"],"Weight":"8.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Horsea candies"},"Next evolution(s)":[{"Number":"117","Name":"Seadra"}]},{"Number":"117","Name":"Seadra","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Dragon Breath","Water Gun"],"Weight":"25.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"116","Name":"Horsea"}]},{"Number":"118","Name":"Goldeen","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Mud Shot"],"Weight":"15.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Goldeen candies"},"Next evolution(s)":[{"Number":"119","Name":"Seaking"}]},{"Number":"119","Name":"Seaking","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Poison Jab"],"Weight":"39.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"118","Name":"Goldeen"}]},{"Number":"120","Name":"Staryu","Classification":"Starshape Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"34.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Staryu candies"},"Next evolution(s)":[{"Number":"120","Name":"Staryu"}]},{"Number":"121","Name":"Starmie","Classification":"Mysterious Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"80.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"121","Name":"Starmie"}]},{"Number":"122","Name":"Mr. Mime","Classification":"Barrier Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"54.5 kg","Height":"1.3 m"},{"Number":"123","Name":"Scyther","Classification":"Mantis Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Steel Wing"],"Weight":"56.0 kg","Height":"1.5 m"},{"Number":"124","Name":"Jynx","Classification":"Humanshape Pokemon","Type I":["Ice"],"Type II":["Psychic"],"Weaknesses":["Fire","Bug","Rock","Ghost","Dark","Steel"],"Fast Attack(s)":["Frost Breath","Pound"],"Weight":"40.6 kg","Height":"1.4 m"},{"Number":"125","Name":"Electabuzz","Classification":"Electric Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Low Kick","Thunder Shock"],"Weight":"30.0 kg","Height":"1.1 m"},{"Number":"126","Name":"Magmar","Classification":"Spitfire Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Karate Chop"],"Weight":"44.5 kg","Height":"1.3 m"},{"Number":"127","Name":"Pinsir","Classification":"Stagbeetle Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Rock Smash"],"Weight":"55.0 kg","Height":"1.5 m"},{"Number":"128","Name":"Tauros","Classification":"Wild Bull Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Tackle","Zen Headbutt"],"Weight":"88.4 kg","Height":"1.4 m"},{"Number":"129","Name":"Magikarp","Classification":"Fish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Splash",""],"Weight":"10.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":400,"Name":"Magikarp candies"},"Next evolution(s)":[{"Number":"130","Name":"Gyarados"}]},{"Number":"130","Name":"Gyarados","Classification":"Atrocious Pokemon","Type I":["Water"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Bite","Dragon Breath"],"Weight":"235.0 kg","Height":"6.5 m","Previous evolution(s)":[{"Number":"129","Name":"Magikarp"}]},{"Number":"131","Name":"Lapras","Classification":"Transport Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"220.0 kg","Height":"2.5 m"},{"Number":"132","Name":"Ditto","Classification":"Transform Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.3 m"},{"Number":"133","Name":"Eevee","Classification":"Evolution Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"6.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Eevee candies"},"Next evolution(s)":[{"Number":"134","Name":"Vaporeon"},{"Number":"135","Name":"Jolteon"},{"Number":"136","Name":"Flareon"}]},{"Number":"134","Name":"Vaporeon","Classification":"Bubble Jet Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun",""],"Weight":"29.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"135","Name":"Jolteon","Classification":"Lightning Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock",""],"Weight":"24.5 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"136","Name":"Flareon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"25.0 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"137","Name":"Porygon","Classification":"Virtual Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"36.5 kg","Height":"0.8 m"},{"Number":"138","Name":"Omanyte","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Water Gun",""],"Weight":"7.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Omanyte candies"},"Next evolution(s)":[{"Number":"139","Name":"Omastar"}]},{"Number":"139","Name":"Omastar","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Rock Throw","Water Gun"],"Weight":"35.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"138","Name":"Omanyte"}]},{"Number":"140","Name":"Kabuto","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"11.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Kabuto candies"},"Next evolution(s)":[{"Number":"141","Name":"Kabutops"}]},{"Number":"141","Name":"Kabutops","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Fury Cutter","Mud Shot"],"Weight":"40.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"140","Name":"Kabuto"}]},{"Number":"142","Name":"Aerodactyl","Classification":"Fossil Pokemon","Type I":["Rock"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Ice","Rock","Steel"],"Fast Attack(s)":["Bite","Steel Wing"],"Weight":"59.0 kg","Height":"1.8 m"},{"Number":"143","Name":"Snorlax","Classification":"Sleeping Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"460.0 kg","Height":"2.1 m"},{"Number":"144","Name":"Articuno","Classification":"Freeze Pokemon","Type I":["Ice"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Rock","Steel"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"55.4 kg","Height":"1.7 m"},{"Number":"145","Name":"Zapdos","Classification":"Electric Pokemon","Type I":["Electric"],"Type II":["Flying"],"Weaknesses":["Ice","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"52.6 kg","Height":"1.6 m"},{"Number":"146","Name":"Moltres","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"60.0 kg","Height":"2.0 m"},{"Number":"147","Name":"Dratini","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"3.3 kg","Height":"1.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Dratini candies"}},{"Number":"148","Name":"Dragonair","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"16.5 kg","Height":"4.0 m","Next Evolution Requirements":{"Amount":100,"Name":"Dratini candies"},"Next evolution(s)":[{"Number":"149","Name":"Dragonite"}]},{"Number":"149","Name":"Dragonite","Classification":"Dragon Pokemon","Type I":["Dragon"],"Type II":["Flying"],"Weaknesses":["Ice","Rock","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath","Steel Wing"],"Weight":"210.0 kg","Height":"2.2 m","Previous evolution(s)":[{"Number":"148","Name":"Dragonair"}]},{"Number":"150","Name":"Mewtwo","Classification":"Genetic Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"122.0 kg","Height":"2.0 m"},{"Number":"151","Name":"Mew","Classification":"New Species Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.4 m"}] \ No newline at end of file +[{"Number":"001","Name":"Bulbasaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Tackle","Vine Whip"],"Weight":"6.9 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"002","Name":"Ivysaur"},{"Number":"003","Name":"Venusaur"}]},{"Number":"002","Name":"Ivysaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"13.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"003","Name":"Venusaur"}]},{"Number":"003","Name":"Venusaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"100.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"},{"Number":"002","Name":"Ivysaur"}]},{"Number":"004","Name":"Charmander","Classification":"Lizard Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Scratch"],"Weight":"8.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"005","Name":"Charmeleon"},{"Number":"006","Name":"Charizard"}]},{"Number":"005","Name":"Charmeleon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"19.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"}],"Next Evolution Requirements":{"Amount":100,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"006","Name":"Charizard"}]},{"Number":"006","Name":"Charizard","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Ember","Wing Attack"],"Weight":"90.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"},{"Number":"005","Name":"Charmeleon"}]},{"Number":"007","Name":"Squirtle","Classification":"Tiny Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Tackle","Bubble"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"008","Name":"Wartortle"},{"Number":"009","Name":"Blastoise"}]},{"Number":"008","Name":"Wartortle","Classification":"Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"22.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"}],"Next Evolution Requirements":{"Amount":100,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"009","Name":"Blastoise"}]},{"Number":"009","Name":"Blastoise","Classification":"Shellfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"85.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"},{"Number":"008","Name":"Wartortle"}]},{"Number":"010","Name":"Caterpie","Classification":"Worm Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"2.9 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"011","Name":"Metapod"},{"Number":"012","Name":"Butterfree"}]},{"Number":"011","Name":"Metapod","Classification":"Cocoon Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"9.9 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"}],"Next Evolution Requirements":{"Amount":50,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"012","Name":"Butterfree"}]},{"Number":"012","Name":"Butterfree","Classification":"Butterfly Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"32.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"},{"Number":"011","Name":"Metapod"}]},{"Number":"013","Name":"Weedle","Classification":"Hairy Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Sting"],"Weight":"3.2 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"014","Name":"Kakuna"},{"Number":"015","Name":"Beedrill"}]},{"Number":"014","Name":"Kakuna","Classification":"Cocoon Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Posion Sting"],"Weight":"10.0 kg","Height":"0.6 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"}],"Next Evolution Requirements":{"Amount":50,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"015","Name":"Beedrill"}]},{"Number":"015","Name":"Beedrill","Classification":"Poison Bee Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Jab"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"},{"Number":"014","Name":"Kakuna"}]},{"Number":"016","Name":"Pidgey","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"1.8 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"017","Name":"Pidgeotto"},{"Number":"018","Name":"Pidgeot"}]},{"Number":"017","Name":"Pidgeotto","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"30.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"}],"Next Evolution Requirements":{"Amount":50,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"018","Name":"Pidgeot"}]},{"Number":"018","Name":"Pidgeot","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Hurricane"],"Weight":"39.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"},{"Number":"017","Name":"Pidgeotto"}]},{"Number":"019","Name":"Rattata","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Body Slam","Dig","Hyper Fang"],"Weight":"3.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Rattata candies"},"Next evolution(s)":[{"Number":"020","Name":"Raticate"}]},{"Number":"020","Name":"Raticate","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Quick Attack"],"Special Attack(s)":["Dig","Hyper Beam","Hyper Fang"],"Weight":"18.5 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"019","Name":"Rattata"}]},{"Number":"021","Name":"Spearow","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"2.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Spearow candies"},"Next evolution(s)":[{"Number":"022","Name":"Fearow"}]},{"Number":"022","Name":"Fearow","Classification":"Beak Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Steel Wing"],"Weight":"38.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"021","Name":"Spearow"}]},{"Number":"023","Name":"Ekans","Classification":"Snake Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Sting"],"Weight":"6.9 kg","Height":"2.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ekans candies"},"Next evolution(s)":[{"Number":"024","Name":"Arbok"}]},{"Number":"024","Name":"Arbok","Classification":"Cobra Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Bite"],"Weight":"65.0 kg","Height":"3.5 m","Previous evolution(s)":[{"Number":"023","Name":"Ekans"}]},{"Number":"025","Name":"Pikachu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Quick Attack","Thunder Shock"],"Weight":"6.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Pikachu candies"},"Next evolution(s)":[{"Number":"026","Name":"Raichu"}]},{"Number":"026","Name":"Raichu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock","Spark"],"Weight":"30.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"025","Name":"Pikachu"}]},{"Number":"027","Name":"Sandshrew","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"12.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Sandshrew candies"},"Next evolution(s)":[{"Number":"028","Name":"Sandslash"}]},{"Number":"028","Name":"Sandslash","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"027","Name":"Sandshrew"}]},{"Number":"029","Name":"Nidoran F","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"7.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"030","Name":"Nidorina"},{"Number":"031","Name":"Nidoqueen"}]},{"Number":"030","Name":"Nidorina","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"20.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"031","Name":"Nidoqueen"}]},{"Number":"031","Name":"Nidoqueen","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"},{"Number":"030","Name":"Nidorina"}]},{"Number":"032","Name":"Nidoran M","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Peck","Poison Sting"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"033","Name":"Nidorino"},{"Number":"034","Name":"Nidoking"}]},{"Number":"033","Name":"Nidorino","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"19.5 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"034","Name":"Nidoking"}]},{"Number":"034","Name":"Nidoking","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Fury Cutter","Poison Jab"],"Weight":"62.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"},{"Number":"033","Name":"Nidorino"}]},{"Number":"035","Name":"Clefairy","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"7.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Clefairy candies"},"Next evolution(s)":[{"Number":"036","Name":"Clefable"}]},{"Number":"036","Name":"Clefable","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"40.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"035","Name":"Clefairy"}]},{"Number":"037","Name":"Vulpix","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"9.9 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Vulpix candies"},"Next evolution(s)":[{"Number":"038","Name":"Ninetales"}]},{"Number":"038","Name":"Ninetales","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"19.9 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"037","Name":"Vulpix"}]},{"Number":"039","Name":"Jigglypuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"5.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Jigglypuff candies"},"Next evolution(s)":[{"Number":"039","Name":"Jigglypuff"}]},{"Number":"040","Name":"Wigglytuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"12.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"040","Name":"Wigglytuff"}]},{"Number":"041","Name":"Zubat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Quick Attack"],"Weight":"7.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Zubat candies"},"Next evolution(s)":[{"Number":"042","Name":"Golbat"}]},{"Number":"042","Name":"Golbat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Wing Attack"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"041","Name":"Zubat"}]},{"Number":"043","Name":"Oddish","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"5.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"044","Name":"Gloom"},{"Number":"045","Name":"Vileplume"}]},{"Number":"044","Name":"Gloom","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"8.6 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"}],"Next Evolution Requirements":{"Amount":100,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"045","Name":"Vileplume"}]},{"Number":"045","Name":"Vileplume","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid",""],"Weight":"18.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"},{"Number":"044","Name":"Gloom"}]},{"Number":"046","Name":"Paras","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Scratch"],"Weight":"5.4 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Paras candies"},"Next evolution(s)":[{"Number":"047","Name":"Parasect"}]},{"Number":"047","Name":"Parasect","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Fury Cutter"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"046","Name":"Paras"}]},{"Number":"048","Name":"Venonat","Classification":"Insect Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Venonat candies"},"Next evolution(s)":[{"Number":"049","Name":"Venomoth"}]},{"Number":"049","Name":"Venomoth","Classification":"Poison Moth Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"12.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"048","Name":"Venonat"}]},{"Number":"050","Name":"Diglett","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"0.8 kg","Height":"0.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Diglett candies"},"Next evolution(s)":[{"Number":"051","Name":"Dugtrio"}]},{"Number":"051","Name":"Dugtrio","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Sucker Punch"],"Weight":"33.3 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"050","Name":"Diglett"}]},{"Number":"052","Name":"Meowth","Classification":"Scratch Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Scratch"],"Weight":"4.2 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Meowth candies"},"Next evolution(s)":[{"Number":"053","Name":"Persian"}]},{"Number":"053","Name":"Persian","Classification":"Classy Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Scratch"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"052","Name":"Meowth"}]},{"Number":"054","Name":"Psyduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun","Zen Headbutt"],"Weight":"19.6 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Psyduck candies"},"Next evolution(s)":[{"Number":"055","Name":"Golduck"}]},{"Number":"055","Name":"Golduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"76.6 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"054","Name":"Psyduck"}]},{"Number":"056","Name":"Mankey","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Scratch"],"Weight":"28.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Mankey candies"},"Next evolution(s)":[{"Number":"057","Name":"Primeape"}]},{"Number":"057","Name":"Primeape","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"056","Name":"Mankey"}]},{"Number":"058","Name":"Growlithe","Classification":"Puppy Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Ember"],"Weight":"19.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":50,"Name":"Growlithe candies"},"Next evolution(s)":[{"Number":"059","Name":"Arcanine"}]},{"Number":"059","Name":"Arcanine","Classification":"Legendary Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Fire Fang"],"Weight":"155.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"058","Name":"Growlithe"}]},{"Number":"060","Name":"Poliwag","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"12.4 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"061","Name":"Poliwhirl"},{"Number":"062","Name":"Poliwrath"}]},{"Number":"061","Name":"Poliwhirl","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"20.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"}],"Next Evolution Requirements":{"Amount":100,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"062","Name":"Poliwrath"}]},{"Number":"062","Name":"Poliwrath","Classification":"Tadpole Pokemon","Type I":["Water"],"Type II":["Fighting"],"Weaknesses":["Electric","Grass","Flying","Psychic","Fairy"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"54.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"},{"Number":"061","Name":"Poliwhirl"}]},{"Number":"063","Name":"Abra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Zen Headbutt",""],"Weight":"19.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":25,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"064","Name":"Kadabra"},{"Number":"065","Name":"Alakazam"}]},{"Number":"064","Name":"Kadabra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"56.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"}],"Next Evolution Requirements":{"Amount":100,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"065","Name":"Alakazam"}]},{"Number":"065","Name":"Alakazam","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"48.0 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"},{"Number":"064","Name":"Kadabra"}]},{"Number":"066","Name":"Machop","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"19.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"067","Name":"Machoke"},{"Number":"068","Name":"Machamp"}]},{"Number":"067","Name":"Machoke","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"70.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"}],"Next Evolution Requirements":{"Amount":100,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"068","Name":"Machamp"}]},{"Number":"068","Name":"Machamp","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Karate Chop"],"Weight":"130.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"},{"Number":"067","Name":"Machoke"}]},{"Number":"069","Name":"Bellsprout","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Vine Whip"],"Weight":"4.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"070","Name":"Weepinbell"},{"Number":"071","Name":"Victreebel"}]},{"Number":"070","Name":"Weepinbell","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"6.4 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"071","Name":"Victreebel"}]},{"Number":"071","Name":"Victreebel","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"15.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"},{"Number":"070","Name":"Weepinbell"}]},{"Number":"072","Name":"Tentacool","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Bubble","Poison Sting"],"Weight":"45.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Tentacool candies"},"Next evolution(s)":[{"Number":"073","Name":"Tentacruel"}]},{"Number":"073","Name":"Tentacruel","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Jab"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"072","Name":"Tentacool"}]},{"Number":"074","Name":"Geodude","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"20.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"075","Name":"Graveler"},{"Number":"076","Name":"Golem"}]},{"Number":"075","Name":"Graveler","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"105.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"}],"Next Evolution Requirements":{"Amount":100,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"076","Name":"Golem"}]},{"Number":"076","Name":"Golem","Classification":"Megaton Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"300.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"},{"Number":"075","Name":"Graveler"}]},{"Number":"077","Name":"Ponyta","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Tackle"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ponyta candies"},"Next evolution(s)":[{"Number":"078","Name":"Rapidash"}]},{"Number":"078","Name":"Rapidash","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Low Kick"],"Weight":"95.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"077","Name":"Ponyta"}]},{"Number":"079","Name":"Slowpoke","Classification":"Dopey Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"36.0 kg","Height":"1.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Slowpoke candies"},"Next evolution(s)":[{"Number":"080","Name":"Slowbro"}]},{"Number":"080","Name":"Slowbro","Classification":"Hermit Crab Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"78.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"079","Name":"Slowpoke"}]},{"Number":"081","Name":"Magnemite","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"6.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Magnemite candies"},"Next evolution(s)":[{"Number":"082","Name":"Magneton"}]},{"Number":"082","Name":"Magneton","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"60.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"081","Name":"Magnemite"}]},{"Number":"083","Name":"Farfetch'd","Classification":"Wild Duck Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"15.0 kg","Height":"0.8 m"},{"Number":"084","Name":"Doduo","Classification":"Twin Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"39.2 kg","Height":"1.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Doduo candies"},"Next evolution(s)":[{"Number":"085","Name":"Dodrio"}]},{"Number":"085","Name":"Dodrio","Classification":"Triple Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Feint Attack","Steel Wing"],"Weight":"85.2 kg","Height":"1.8 m","Previous evolution(s)":[{"Number":"084","Name":"Doduo"}]},{"Number":"086","Name":"Seel","Classification":"Sea Lion Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Water Gun"],"Weight":"90.0 kg","Height":"1.1 m","Next Evolution Requirements":{"Amount":50,"Name":"Seel candies"},"Next evolution(s)":[{"Number":"087","Name":"Dewgong"}]},{"Number":"087","Name":"Dewgong","Classification":"Sea Lion Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"120.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"086","Name":"Seel"}]},{"Number":"088","Name":"Grimer","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Mud Slap"],"Weight":"30.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Grimer candies"},"Next evolution(s)":[{"Number":"089","Name":"Muk"}]},{"Number":"089","Name":"Muk","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Poison Jab",""],"Weight":"30.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"088","Name":"Grimer"}]},{"Number":"090","Name":"Shellder","Classification":"Bivalve Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Tackle"],"Weight":"4.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Shellder candies"},"Next evolution(s)":[{"Number":"091","Name":"Cloyster"}]},{"Number":"091","Name":"Cloyster","Classification":"Bivalve Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"132.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"090","Name":"Shellder"}]},{"Number":"092","Name":"Gastly","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Sucker Punch"],"Weight":"0.1 kg","Height":"1.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"093","Name":"Haunter"},{"Number":"094","Name":"Gengar"}]},{"Number":"093","Name":"Haunter","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Shadow Claw"],"Weight":"0.1 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"}],"Next Evolution Requirements":{"Amount":100,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"094","Name":"Gengar"}]},{"Number":"094","Name":"Gengar","Classification":"Shadow Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Shadow Claw","Sucker Punch"],"Weight":"40.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"},{"Number":"093","Name":"Haunter"}]},{"Number":"095","Name":"Onix","Classification":"Rock Snake Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"210.0 kg","Height":"8.8 m"},{"Number":"096","Name":"Drowzee","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Pound"],"Weight":"32.4 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Drowzee candies"},"Next evolution(s)":[{"Number":"097","Name":"Hypno"}]},{"Number":"097","Name":"Hypno","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"75.6 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"096","Name":"Drowzee"}]},{"Number":"098","Name":"Krabby","Classification":"River Crab Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Krabby candies"},"Next evolution(s)":[{"Number":"099","Name":"Kingler"}]},{"Number":"099","Name":"Kingler","Classification":"Pincer Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"098","Name":"Krabby"}]},{"Number":"100","Name":"Voltorb","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark","Tackle"],"Weight":"10.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Voltorb candies"},"Next evolution(s)":[{"Number":"101","Name":"Electrode"}]},{"Number":"101","Name":"Electrode","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark",""],"Weight":"66.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"100","Name":"Voltorb"}]},{"Number":"102","Name":"Exeggcute","Classification":"Egg Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion",""],"Weight":"2.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Exeggcute candies"},"Next evolution(s)":[{"Number":"103","Name":"Exeggutor"}]},{"Number":"103","Name":"Exeggutor","Classification":"Coconut Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"120.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"102","Name":"Exeggcute"}]},{"Number":"104","Name":"Cubone","Classification":"Lonely Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Cubone candies"},"Next evolution(s)":[{"Number":"105","Name":"Marowak"}]},{"Number":"105","Name":"Marowak","Classification":"Bone Keeper Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"45.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"104","Name":"Cubone"}]},{"Number":"106","Name":"Hitmonlee","Classification":"Kicking Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Low Kick","Rock Smash"],"Weight":"49.8 kg","Height":"1.5 m","Next evolution(s)":[{"Number":"107","Name":"Hitmonchan"}]},{"Number":"107","Name":"Hitmonchan","Classification":"Punching Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Rock Smash"],"Weight":"50.2 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"106","Name":"Hitmonlee"}]},{"Number":"108","Name":"Lickitung","Classification":"Licking Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"65.5 kg","Height":"1.2 m"},{"Number":"109","Name":"Koffing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"1.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Koffing candies"},"Next evolution(s)":[{"Number":"110","Name":"Weezing"}]},{"Number":"110","Name":"Weezing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"9.5 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"109","Name":"Koffing"}]},{"Number":"111","Name":"Rhyhorn","Classification":"Spikes Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"115.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Rhyhorn candies"},"Next evolution(s)":[{"Number":"112","Name":"Rhydon"}]},{"Number":"112","Name":"Rhydon","Classification":"Drill Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"120.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"111","Name":"Rhyhorn"}]},{"Number":"113","Name":"Chansey","Classification":"Egg Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"34.6 kg","Height":"1.1 m"},{"Number":"114","Name":"Tangela","Classification":"Vine Pokemon","Type I":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug"],"Fast Attack(s)":["Vine Whip",""],"Weight":"35.0 kg","Height":"1.0 m"},{"Number":"115","Name":"Kangaskhan","Classification":"Parent Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Low Kick",""],"Weight":"80.0 kg","Height":"2.2 m"},{"Number":"116","Name":"Horsea","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Water Gun"],"Weight":"8.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Horsea candies"},"Next evolution(s)":[{"Number":"117","Name":"Seadra"}]},{"Number":"117","Name":"Seadra","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Dragon Breath","Water Gun"],"Weight":"25.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"116","Name":"Horsea"}]},{"Number":"118","Name":"Goldeen","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Mud Shot"],"Weight":"15.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Goldeen candies"},"Next evolution(s)":[{"Number":"119","Name":"Seaking"}]},{"Number":"119","Name":"Seaking","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Poison Jab"],"Weight":"39.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"118","Name":"Goldeen"}]},{"Number":"120","Name":"Staryu","Classification":"Starshape Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"34.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Staryu candies"},"Next evolution(s)":[{"Number":"120","Name":"Staryu"}]},{"Number":"121","Name":"Starmie","Classification":"Mysterious Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"80.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"121","Name":"Starmie"}]},{"Number":"122","Name":"Mr. Mime","Classification":"Barrier Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"54.5 kg","Height":"1.3 m"},{"Number":"123","Name":"Scyther","Classification":"Mantis Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Steel Wing"],"Weight":"56.0 kg","Height":"1.5 m"},{"Number":"124","Name":"Jynx","Classification":"Humanshape Pokemon","Type I":["Ice"],"Type II":["Psychic"],"Weaknesses":["Fire","Bug","Rock","Ghost","Dark","Steel"],"Fast Attack(s)":["Frost Breath","Pound"],"Weight":"40.6 kg","Height":"1.4 m"},{"Number":"125","Name":"Electabuzz","Classification":"Electric Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Low Kick","Thunder Shock"],"Weight":"30.0 kg","Height":"1.1 m"},{"Number":"126","Name":"Magmar","Classification":"Spitfire Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Karate Chop"],"Weight":"44.5 kg","Height":"1.3 m"},{"Number":"127","Name":"Pinsir","Classification":"Stagbeetle Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Rock Smash"],"Weight":"55.0 kg","Height":"1.5 m"},{"Number":"128","Name":"Tauros","Classification":"Wild Bull Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Tackle","Zen Headbutt"],"Weight":"88.4 kg","Height":"1.4 m"},{"Number":"129","Name":"Magikarp","Classification":"Fish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Splash",""],"Weight":"10.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":400,"Name":"Magikarp candies"},"Next evolution(s)":[{"Number":"130","Name":"Gyarados"}]},{"Number":"130","Name":"Gyarados","Classification":"Atrocious Pokemon","Type I":["Water"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Bite","Dragon Breath"],"Weight":"235.0 kg","Height":"6.5 m","Previous evolution(s)":[{"Number":"129","Name":"Magikarp"}]},{"Number":"131","Name":"Lapras","Classification":"Transport Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"220.0 kg","Height":"2.5 m"},{"Number":"132","Name":"Ditto","Classification":"Transform Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.3 m"},{"Number":"133","Name":"Eevee","Classification":"Evolution Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"6.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Eevee candies"},"Next evolution(s)":[{"Number":"134","Name":"Vaporeon"},{"Number":"135","Name":"Jolteon"},{"Number":"136","Name":"Flareon"}]},{"Number":"134","Name":"Vaporeon","Classification":"Bubble Jet Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun",""],"Weight":"29.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"135","Name":"Jolteon","Classification":"Lightning Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock",""],"Weight":"24.5 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"136","Name":"Flareon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"25.0 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"137","Name":"Porygon","Classification":"Virtual Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"36.5 kg","Height":"0.8 m"},{"Number":"138","Name":"Omanyte","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Water Gun",""],"Weight":"7.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Omanyte candies"},"Next evolution(s)":[{"Number":"139","Name":"Omastar"}]},{"Number":"139","Name":"Omastar","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Rock Throw","Water Gun"],"Weight":"35.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"138","Name":"Omanyte"}]},{"Number":"140","Name":"Kabuto","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"11.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Kabuto candies"},"Next evolution(s)":[{"Number":"141","Name":"Kabutops"}]},{"Number":"141","Name":"Kabutops","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Fury Cutter","Mud Shot"],"Weight":"40.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"140","Name":"Kabuto"}]},{"Number":"142","Name":"Aerodactyl","Classification":"Fossil Pokemon","Type I":["Rock"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Ice","Rock","Steel"],"Fast Attack(s)":["Bite","Steel Wing"],"Weight":"59.0 kg","Height":"1.8 m"},{"Number":"143","Name":"Snorlax","Classification":"Sleeping Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"460.0 kg","Height":"2.1 m"},{"Number":"144","Name":"Articuno","Classification":"Freeze Pokemon","Type I":["Ice"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Rock","Steel"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"55.4 kg","Height":"1.7 m"},{"Number":"145","Name":"Zapdos","Classification":"Electric Pokemon","Type I":["Electric"],"Type II":["Flying"],"Weaknesses":["Ice","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"52.6 kg","Height":"1.6 m"},{"Number":"146","Name":"Moltres","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"60.0 kg","Height":"2.0 m"},{"Number":"147","Name":"Dratini","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"3.3 kg","Height":"1.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Dratini candies"}},{"Number":"148","Name":"Dragonair","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"16.5 kg","Height":"4.0 m","Next Evolution Requirements":{"Amount":100,"Name":"Dratini candies"},"Next evolution(s)":[{"Number":"149","Name":"Dragonite"}]},{"Number":"149","Name":"Dragonite","Classification":"Dragon Pokemon","Type I":["Dragon"],"Type II":["Flying"],"Weaknesses":["Ice","Rock","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath","Steel Wing"],"Weight":"210.0 kg","Height":"2.2 m","Previous evolution(s)":[{"Number":"148","Name":"Dragonair"}]},{"Number":"150","Name":"Mewtwo","Classification":"Genetic Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"122.0 kg","Height":"2.0 m"},{"Number":"151","Name":"Mew","Classification":"New Species Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.4 m"}] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..95a32f8ceb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: '2' +services: + bot1-pokego: + build: . + volumes: + - ./configs/config.json:/usr/src/app/configs/config.json + stdin_open: true + tty: true + bot1-pokegoweb: + image: python:2.7 + ports: + - "8000:8000" + volumes_from: + - bot1-pokego + volumes: + - ./configs/userdata.js:/usr/src/app/web/config/userdata.js + working_dir: /usr/src/app/web + command: bash -c "echo 'Serving HTTP on 0.0.0.0 port 8000' && python -m SimpleHTTPServer > /dev/null 2>&1" + depends_on: + - bot1-pokego \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000000..c11992bb0c --- /dev/null +++ b/install.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Setup Python virtualenv +echo "Setting up Python virtualenv..." +eval "virtualenv ." +eval "source bin/activate" +echo "Python virtualenv setup successfully." + +# Install pip requirements +echo "Installing pip requirements..." +eval "pip install -r requirements.txt" +echo "Installed pip requirements." +echo "Installing and updating git submodules..." + +# Install git submodules +eval "cd ./web && git submodule init && cd .." +eval "git submodule update" +echo "Done." +echo "Please create and setup configs/config.json. Then, run 'python pokecli.py --config ./configs/config.json' or './run.sh' on Mac/Linux" \ No newline at end of file diff --git a/pokecli.py b/pokecli.py old mode 100755 new mode 100644 index 963b96bf20..3d1b3736e7 --- a/pokecli.py +++ b/pokecli.py @@ -25,29 +25,134 @@ Author: tjado """ -import os -import re -import json import argparse -import time +import codecs +import json +import logging +import os import ssl import sys -import codecs +import time +from datetime import timedelta from getpass import getpass -import logging -import requests -from pokemongo_bot import logger -from pokemongo_bot import PokemonGoBot -from pokemongo_bot.cell_workers.utils import print_green, print_yellow, print_red +from pgoapi.exceptions import NotLoggedInException, ServerSideRequestThrottlingException, ServerBusyOrOfflineException +from geopy.exc import GeocoderQuotaExceeded + +from pokemongo_bot import PokemonGoBot, TreeConfigBuilder +from pokemongo_bot.health_record import BotEvent if sys.version_info >= (2, 7, 9): ssl._create_default_https_context = ssl._create_unverified_context +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s [%(name)10s] [%(levelname)s] %(message)s') +logger = logging.getLogger('cli') +logger.setLevel(logging.INFO) + +def main(): + try: + logger.info('PokemonGO Bot v1.0') + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + sys.stderr = codecs.getwriter('utf8')(sys.stderr) + + config = init_config() + if not config: + return + + logger.info('Configuration initialized') + health_record = BotEvent(config) + health_record.login_success() + + finished = False + + while not finished: + try: + bot = PokemonGoBot(config) + bot.start() + tree = TreeConfigBuilder(bot, config.raw_tasks).build() + bot.workers = tree + bot.metrics.capture_stats() + + bot.event_manager.emit( + 'bot_start', + sender=bot, + level='info', + formatted='Starting bot...' + ) + + while True: + bot.tick() + + except KeyboardInterrupt: + bot.event_manager.emit( + 'bot_exit', + sender=bot, + level='info', + formatted='Exiting bot.' + ) + finished = True + report_summary(bot) + + except NotLoggedInException: + wait_time = config.reconnecting_timeout * 60 + bot.event_manager.emit( + 'api_error', + sender=bot, + level='info', + formmated='Log logged in, reconnecting in {:s}'.format(wait_time) + ) + time.sleep(wait_time) + except ServerBusyOrOfflineException: + bot.event_manager.emit( + 'api_error', + sender=bot, + level='info', + formatted='Server busy or offline' + ) + except ServerSideRequestThrottlingException: + bot.event_manager.emit( + 'api_error', + sender=bot, + level='info', + formatted='Server is throttling, reconnecting in 30 seconds' + ) + time.sleep(30) + + except GeocoderQuotaExceeded: + raise Exception("Google Maps API key over requests limit.") + except Exception as e: + # always report session summary and then raise exception + if bot: + report_summary(bot) + + raise e + +def report_summary(bot): + if bot.metrics.start_time is None: + return # Bot didn't actually start, no metrics to show. + + metrics = bot.metrics + metrics.capture_stats() + logger.info('') + logger.info('Ran for {}'.format(metrics.runtime())) + logger.info('Total XP Earned: {} Average: {:.2f}/h'.format(metrics.xp_earned(), metrics.xp_per_hour())) + logger.info('Travelled {:.2f}km'.format(metrics.distance_travelled())) + logger.info('Visited {} stops'.format(metrics.visits['latest'] - metrics.visits['start'])) + logger.info('Encountered {} pokemon, {} caught, {} released, {} evolved, {} never seen before' + .format(metrics.num_encounters(), metrics.num_captures(), metrics.releases, + metrics.num_evolutions(), metrics.num_new_mons())) + logger.info('Threw {} pokeball{}'.format(metrics.num_throws(), '' if metrics.num_throws() == 1 else 's')) + logger.info('Earned {} Stardust'.format(metrics.earned_dust())) + logger.info('') + if metrics.highest_cp is not None: + logger.info('Highest CP Pokemon: {}'.format(metrics.highest_cp['desc'])) + if metrics.most_perfect is not None: + logger.info('Most Perfect Pokemon: {}'.format(metrics.most_perfect['desc'])) def init_config(): parser = argparse.ArgumentParser() - config_file = "config.json" - release_config_json = "release_config.json" + config_file = "configs/config.json" web_dir = "web" # If config file exists, load variables from json @@ -55,165 +160,333 @@ def init_config(): # Select a config file code parser.add_argument("-cf", "--config", help="Config File to use") - config_arg = unicode(parser.parse_args().config) - if os.path.isfile(config_arg): + config_arg = parser.parse_known_args() and parser.parse_known_args()[0].config or None + if config_arg and os.path.isfile(config_arg): with open(config_arg) as data: load.update(json.load(data)) elif os.path.isfile(config_file): + logger.info('No config argument specified, checking for /configs/config.json') with open(config_file) as data: load.update(json.load(data)) + else: + logger.info('Error: No /configs/config.json or specified config') # Read passed in Arguments required = lambda x: not x in load - parser.add_argument("-a", - "--auth_service", - help="Auth Service ('ptc' or 'google')", - required=required("auth_service")) - parser.add_argument("-u", "--username", help="Username") - parser.add_argument("-p", "--password", help="Password") - parser.add_argument("-l", "--location", help="Location") - parser.add_argument("-lc", - "--location_cache", - help="Bot will start at last known location", - type=bool, - default=False) - parser.add_argument("-m", - "--mode", - help="Farming Mode", - type=str, - default="all") - parser.add_argument( - "-w", - "--walk", + add_config( + parser, + load, + short_flag="-a", + long_flag="--auth_service", + help="Auth Service ('ptc' or 'google')", + required=required("auth_service"), + default=None + ) + add_config( + parser, + load, + short_flag="-u", + long_flag="--username", + help="Username", + default=None + ) + add_config( + parser, + load, + short_flag="-ws", + long_flag="--websocket.server_url", + help="Connect to websocket server at given url", + default=False + ) + add_config( + parser, + load, + short_flag="-wss", + long_flag="--websocket.start_embedded_server", + help="Start embedded websocket server", + default=False + ) + add_config( + parser, + load, + short_flag="-wsr", + long_flag="--websocket.remote_control", + help="Enable remote control through websocket (requires websocekt server url)", + default=False + ) + add_config( + parser, + load, + short_flag="-p", + long_flag="--password", + help="Password", + default=None + ) + add_config( + parser, + load, + short_flag="-l", + long_flag="--location", + help="Location", + type=parse_unicode_str, + default='' + ) + add_config( + parser, + load, + short_flag="-lc", + long_flag="--location_cache", + help="Bot will start at last known location", + type=bool, + default=False + ) + add_config( + parser, + load, + long_flag="--forts.spin", + help="Enable Spinning Pokestops", + type=bool, + default=True, + ) + add_config( + parser, + load, + short_flag="-w", + long_flag="--walk", help= "Walk instead of teleport with given speed (meters per second, e.g. 2.5)", type=float, - default=2.5) - parser.add_argument("-k", - "--gmapkey", - help="Set Google Maps API KEY", - type=str, - default=None) - parser.add_argument( - "-ms", - "--max_steps", - help= - "Set the steps around your initial location(DEFAULT 5 mean 25 cells around your location)", - type=int, - default=50) - parser.add_argument( - "-it", - "--initial_transfer", - help= - "Transfer all duplicate pokemon with same ID on bot start, except pokemon with highest CP. Accepts a number to prevent transferring pokemon with a CP above the provided value. Default is 0 (aka transfer none).", - type=int, - default=0) - parser.add_argument("-d", - "--debug", - help="Debug Mode", - type=bool, - default=False) - parser.add_argument("-t", - "--test", - help="Only parse the specified location", - type=bool, - default=False) - parser.add_argument( - "-du", - "--distance_unit", - help= - "Set the unit to display distance in (e.g, km for kilometers, mi for miles, ft for feet)", + default=2.5 + ) + add_config( + parser, + load, + short_flag="-k", + long_flag="--gmapkey", + help="Set Google Maps API KEY", type=str, - default="km") - - parser.add_argument( - "-if", - "--item_filter", - help= - "Pass a list of unwanted items to recycle when collected at a Pokestop (e.g, \"101,102,103,104\" to recycle potions when collected)", + default=None + ) + add_config( + parser, + load, + short_flag="-e", + long_flag="--show_events", + help="Show events", + type=bool, + default=False + ) + add_config( + parser, + load, + short_flag="-d", + long_flag="--debug", + help="Debug Mode", + type=bool, + default=False + ) + add_config( + parser, + load, + short_flag="-t", + long_flag="--test", + help="Only parse the specified location", + type=bool, + default=False + ) + add_config( + parser, + load, + short_flag="-du", + long_flag="--distance_unit", + help="Set the unit to display distance in (e.g, km for kilometers, mi for miles, ft for feet)", type=str, - default=[]) - - parser.add_argument("-ev", - "--evolve_all", - help="(Batch mode) Pass \"all\" or a list of pokemons to evolve (e.g., \"Pidgey,Weedle,Caterpie\"). Bot will start by attempting to evolve all pokemons. Great after popping a lucky egg!", - type=str, - default=[]) - - parser.add_argument("-ec", - "--evolve_captured", - help="(Ad-hoc mode) Bot will attempt to evolve all the pokemons captured!", - type=bool, - default=False) + default='km' + ) + add_config( + parser, + load, + short_flag="-ec", + long_flag="--evolve_captured", + help="(Ad-hoc mode) Pass \"all\" or a list of pokemon to evolve (e.g., \"Pidgey,Weedle,Caterpie\"). Bot will attempt to evolve all the pokemon captured!", + type=str, + default=[] + ) + add_config( + parser, + load, + short_flag="-rt", + long_flag="--reconnecting_timeout", + help="Timeout between reconnecting if error occured (in minutes, e.g. 15)", + type=float, + default=15.0 + ) + add_config( + parser, + load, + short_flag="-hr", + long_flag="--health_record", + help="Send anonymous bot event to GA for bot health record. Set \"health_record\":false if you need disable it.", + type=bool, + default=True + ) + add_config( + parser, + load, + short_flag="-ac", + long_flag="--forts.avoid_circles", + help="Avoids circles (pokestops) of the max size set in max_circle_size flag", + type=bool, + default=False, + ) + add_config( + parser, + load, + short_flag="-mcs", + long_flag="--forts.max_circle_size", + help="If avoid_circles flag is set, this flag specifies the maximum size of circles (pokestops) avoided", + type=int, + default=10, + ) + add_config( + parser, + load, + long_flag="--catch_randomize_reticle_factor", + help="Randomize factor for pokeball throwing accuracy (DEFAULT 1.0 means no randomize: always 'Excellent' throw. 0.0 randomizes between normal and 'Excellent' throw)", + type=float, + default=1.0 + ) + add_config( + parser, + load, + long_flag="--catch_randomize_spin_factor", + help="Randomize factor for pokeball curve throwing (DEFAULT 1.0 means no randomize: always perfect 'Super Spin' curve ball. 0.0 randomizes between normal and 'Super Spin' curve ball)", + type=float, + default=1.0 + ) + add_config( + parser, + load, + long_flag="--map_object_cache_time", + help="Amount of seconds to keep the map object in cache (bypass Niantic throttling)", + type=float, + default=5.0 + ) + # Start to parse other attrs config = parser.parse_args() if not config.username and 'username' not in load: config.username = raw_input("Username: ") if not config.password and 'password' not in load: config.password = getpass("Password: ") - # Passed in arguments should trump - for key in config.__dict__: - if key in load: - config.__dict__[key] = load[key] + config.catch = load.get('catch', {}) + config.release = load.get('release', {}) + config.action_wait_max = load.get('action_wait_max', 4) + config.action_wait_min = load.get('action_wait_min', 1) + config.raw_tasks = load.get('tasks', []) + + config.vips = load.get('vips', {}) + + if config.map_object_cache_time < 0.0: + parser.error("--map_object_cache_time is out of range! (should be >= 0.0)") + return None + + if len(config.raw_tasks) == 0: + logging.error("No tasks are configured. Did you mean to configure some behaviors? Read https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files#configuring-tasks for more information") + return None if config.auth_service not in ['ptc', 'google']: logging.error("Invalid Auth service specified! ('ptc' or 'google')") return None + def task_configuration_error(flag_name): + parser.error(""" + \"{}\" was removed from the configuration options. + You can now change the behavior of the bot by modifying the \"tasks\" key. + Read https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files#configuring-tasks for more information. + """.format(flag_name)) + + old_flags = ['mode', 'catch_pokemon', 'spin_forts', 'forts_spin', 'hatch_eggs', 'release_pokemon', 'softban_fix', + 'longer_eggs_first', 'evolve_speed', 'use_lucky_egg', 'item_filter', 'evolve_all', 'evolve_cp_min', 'max_steps'] + for flag in old_flags: + if flag in load: + task_configuration_error(flag) + return None + + nested_old_flags = [('forts', 'spin'), ('forts', 'move_to_spin'), ('navigator', 'path_mode'), ('navigator', 'path_file'), ('navigator', 'type')] + for outer, inner in nested_old_flags: + if load.get(outer, {}).get(inner, None): + task_configuration_error('{}.{}'.format(outer, inner)) + return None + + if (config.evolve_captured + and (not isinstance(config.evolve_captured, str) + or str(config.evolve_captured).lower() in ["true", "false"])): + parser.error('"evolve_captured" should be list of pokemons: use "all" or "none" to match all ' + + 'or none of the pokemons, or use a comma separated list such as "Pidgey,Weedle,Caterpie"') + return None + if not (config.location or config.location_cache): parser.error("Needs either --use-location-cache or --location.") return None - if config.item_filter: - config.item_filter = [str(item_id) for item_id in config.item_filter.split(',')] + if config.catch_randomize_reticle_factor < 0 or 1 < config.catch_randomize_reticle_factor: + parser.error("--catch_randomize_reticle_factor is out of range! (should be 0 <= catch_randomize_reticle_factor <= 1)") + return None - config.release_config = {} - if os.path.isfile(release_config_json): - with open(release_config_json) as data: - config.release_config.update(json.load(data)) + if config.catch_randomize_spin_factor < 0 or 1 < config.catch_randomize_spin_factor: + parser.error("--catch_randomize_spin_factor is out of range! (should be 0 <= catch_randomize_spin_factor <= 1)") + return None # create web dir if not exists - try: + try: os.makedirs(web_dir) except OSError: if not os.path.isdir(web_dir): raise - if config.evolve_all: - config.evolve_all = [str(pokemon_name) for pokemon_name in config.evolve_all.split(',')] + if config.evolve_captured and isinstance(config.evolve_captured, str): + config.evolve_captured = [str(pokemon_name).strip() for pokemon_name in config.evolve_captured.split(',')] + fix_nested_config(config) return config +def add_config(parser, json_config, short_flag=None, long_flag=None, **kwargs): + if not long_flag: + raise Exception('add_config calls requires long_flag parameter!') -def main(): - # log settings - # log format - #logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(module)10s] [%(levelname)5s] %(message)s') + full_attribute_path = long_flag.split('--')[1] + attribute_name = full_attribute_path.split('.')[-1] - sys.stdout = codecs.getwriter('utf8')(sys.stdout) - sys.stderr = codecs.getwriter('utf8')(sys.stderr) + if '.' in full_attribute_path: # embedded config! + embedded_in = full_attribute_path.split('.')[0: -1] + for level in embedded_in: + json_config = json_config.get(level, {}) - config = init_config() - if not config: - return + if 'default' in kwargs: + kwargs['default'] = json_config.get(attribute_name, kwargs['default']) + if short_flag: + args = (short_flag, long_flag) + else: + args = (long_flag,) + parser.add_argument(*args, **kwargs) - logger.log('[x] PokemonGO Bot v1.0', 'green') - logger.log('[x] Configuration initialized', 'yellow') - try: - bot = PokemonGoBot(config) - bot.start() +def fix_nested_config(config): + config_dict = config.__dict__ - logger.log('[x] Starting PokemonGo Bot....', 'green') + for key, value in config_dict.iteritems(): + if '.' in key: + new_key = key.replace('.', '_') + config_dict[new_key] = value + del config_dict[key] - while True: - bot.take_step() - - except KeyboardInterrupt: - logger.log('[x] Exiting PokemonGo Bot', 'red') - # TODO Add number of pokemon catched, pokestops visited, highest CP - # pokemon catched, etc. +def parse_unicode_str(string): + try: + return string.decode('utf8') + except UnicodeEncodeError: + return string if __name__ == '__main__': diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 6dc13bbbda..045abd82e9 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -1,424 +1,922 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals -import logging -import googlemaps +import datetime import json +import logging +import os import random -import threading -import datetime -import sys -import yaml -import logger import re +import sys +import time + +from geopy.geocoders import GoogleV3 from pgoapi import PGoApi -from cell_workers import PokemonCatchWorker, SeenFortWorker, MoveToFortWorker, InitialTransferWorker, EvolveAllWorker +from pgoapi.utilities import f2i, get_cell_ids + +import cell_workers +from api_wrapper import ApiWrapper from cell_workers.utils import distance +from event_manager import EventManager from human_behaviour import sleep -from stepper import Stepper -from geopy.geocoders import GoogleV3 -from math import radians, sqrt, sin, cos, atan2 from item_list import Item +from metrics import Metrics +from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler +from pokemongo_bot.socketio_server.runner import SocketIoRunner +from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl +from worker_result import WorkerResult +from tree_config_builder import ConfigException, TreeConfigBuilder class PokemonGoBot(object): + @property + def position(self): + return self.api._position_lat, self.api._position_lng, 0 + + @position.setter + def position(self, position_tuple): + self.api._position_lat, self.api._position_lng, self.api._position_alt = position_tuple + def __init__(self, config): self.config = config - self.pokemon_list = json.load(open('data/pokemon.json')) - self.item_list = json.load(open('data/items.json')) + self.fort_timeouts = dict() + self.pokemon_list = json.load( + open(os.path.join('data', 'pokemon.json')) + ) + self.item_list = json.load(open(os.path.join('data', 'items.json'))) + self.metrics = Metrics(self) + self.latest_inventory = None + self.cell = None + self.recent_forts = [None] * config.forts_max_circle_size + self.tick_count = 0 + self.softban = False + self.start_position = None + self.last_map_object = None + self.last_time_map_object = 0 + self.logger = logging.getLogger(type(self).__name__) + + # Make our own copy of the workers for this instance + self.workers = [] def start(self): + self._setup_event_system() self._setup_logging() self._setup_api() - self.stepper = Stepper(self) + random.seed() - def take_step(self): - self.stepper.take_step() - - def work_on_cell(self, cell, position, include_fort_on_path): - if self.config.evolve_all: - # Run evolve all once. Flip the bit. - print('[#] Attempting to evolve all pokemons ...') - worker = EvolveAllWorker(self) - worker.work() - self.config.evolve_all = [] - - self._filter_ignored_pokemons(cell) - - if (self.config.mode == "all" or self.config.mode == - "poke") and 'catchable_pokemons' in cell and len(cell[ - 'catchable_pokemons']) > 0: - logger.log('[#] Something rustles nearby!') - # Sort all by distance from current pos- eventually this should - # build graph & A* it - cell['catchable_pokemons'].sort( - key= - lambda x: distance(self.position[0], self.position[1], x['latitude'], x['longitude'])) - - user_web_catchable = 'web/catchable-%s.json' % (self.config.username) - for pokemon in cell['catchable_pokemons']: - with open(user_web_catchable, 'w') as outfile: - json.dump(pokemon, outfile) - - if self.catch_pokemon(pokemon) == PokemonCatchWorker.NO_POKEBALLS: - break - with open(user_web_catchable, 'w') as outfile: - json.dump({}, outfile) - - if (self.config.mode == "all" or self.config.mode == "poke" - ) and 'wild_pokemons' in cell and len(cell['wild_pokemons']) > 0: - # Sort all by distance from current pos- eventually this should - # build graph & A* it - cell['wild_pokemons'].sort( - key= - lambda x: distance(self.position[0], self.position[1], x['latitude'], x['longitude'])) - for pokemon in cell['wild_pokemons']: - if self.catch_pokemon(pokemon) == PokemonCatchWorker.NO_POKEBALLS: - break - if (self.config.mode == "all" or - self.config.mode == "farm") and include_fort_on_path: - if 'forts' in cell: - # Only include those with a lat/long - forts = [fort - for fort in cell['forts'] - if 'latitude' in fort and 'type' in fort] - gyms = [gym for gym in cell['forts'] if 'gym_points' in gym] - - # Sort all by distance from current pos- eventually this should - # build graph & A* it - forts.sort(key=lambda x: distance(self.position[ - 0], self.position[1], x['latitude'], x['longitude'])) - for fort in forts: - worker = MoveToFortWorker(fort, self) - worker.work() - - worker = SeenFortWorker(fort, self) - hack_chain = worker.work() - if hack_chain > 10: - #print('need a rest') - break + def _setup_event_system(self): + handlers = [LoggingHandler()] + if self.config.websocket_server_url: + if self.config.websocket_start_embedded_server: + self.sio_runner = SocketIoRunner(self.config.websocket_server_url) + self.sio_runner.start_listening_async() + + websocket_handler = SocketIoHandler( + self, + self.config.websocket_server_url + ) + handlers.append(websocket_handler) + + if self.config.websocket_remote_control: + remote_control = WebsocketRemoteControl(self).start() + + + self.event_manager = EventManager(*handlers) + self._register_events() + if self.config.show_events: + self.event_manager.event_report() + sys.exit(1) + + # Registering event: + # self.event_manager.register_event("location", parameters=['lat', 'lng']) + # + # Emitting event should be enough to add logging and send websocket + # message: : + # self.event_manager.emit('location', 'level'='info', data={'lat': 1, 'lng':1}), + + def _register_events(self): + self.event_manager.register_event( + 'location_found', + parameters=('position', 'location') + ) + self.event_manager.register_event('api_error') + self.event_manager.register_event('config_error') + + self.event_manager.register_event('login_started') + self.event_manager.register_event('login_failed') + self.event_manager.register_event('login_successful') + + self.event_manager.register_event('set_start_location') + self.event_manager.register_event('load_cached_location') + self.event_manager.register_event('location_cache_ignored') + self.event_manager.register_event( + 'position_update', + parameters=( + 'current_position', + 'last_position', + 'distance', # optional + 'distance_unit' # optional + ) + ) + self.event_manager.register_event('location_cache_error') + + self.event_manager.register_event('bot_start') + self.event_manager.register_event('bot_exit') + + # sleep stuff + self.event_manager.register_event( + 'next_sleep', + parameters=('time',) + ) + self.event_manager.register_event( + 'bot_sleep', + parameters=('time_in_seconds',) + ) + + # fort stuff + self.event_manager.register_event( + 'spun_fort', + parameters=( + 'fort_id', + 'latitude', + 'longitude' + ) + ) + self.event_manager.register_event( + 'lured_pokemon_found', + parameters=( + 'fort_id', + 'fort_name', + 'encounter_id', + 'latitude', + 'longitude' + ) + ) + self.event_manager.register_event( + 'moving_to_fort', + parameters=( + 'fort_name', + 'distance' + ) + ) + self.event_manager.register_event( + 'moving_to_lured_fort', + parameters=( + 'fort_name', + 'distance', + 'lure_distance' + ) + ) + self.event_manager.register_event( + 'spun_pokestop', + parameters=( + 'pokestop', 'exp', 'items' + ) + ) + self.event_manager.register_event( + 'pokestop_empty', + parameters=('pokestop',) + ) + self.event_manager.register_event( + 'pokestop_out_of_range', + parameters=('pokestop',) + ) + self.event_manager.register_event( + 'pokestop_on_cooldown', + parameters=('pokestop', 'minutes_left') + ) + self.event_manager.register_event( + 'unknown_spin_result', + parameters=('status_code',) + ) + self.event_manager.register_event('pokestop_searching_too_often') + self.event_manager.register_event('arrived_at_fort') + + # pokemon stuff + self.event_manager.register_event( + 'catchable_pokemon', + parameters=( + 'pokemon_id', + 'spawn_point_id', + 'encounter_id', + 'latitude', + 'longitude', + 'expiration_timestamp_ms' + ) + ) + self.event_manager.register_event( + 'pokemon_appeared', + parameters=( + 'pokemon', + 'cp', + 'iv', + 'iv_display', + ) + ) + self.event_manager.register_event( + 'pokemon_catch_rate', + parameters=( + 'catch_rate', + 'berry_name', + 'berry_count' + ) + ) + self.event_manager.register_event( + 'threw_berry', + parameters=( + 'berry_name', + 'new_catch_rate' + ) + ) + self.event_manager.register_event( + 'threw_pokeball', + parameters=( + 'pokeball', + 'success_percentage', + 'count_left' + ) + ) + self.event_manager.register_event( + 'pokemon_fled', + parameters=('pokemon',) + ) + self.event_manager.register_event( + 'pokemon_vanished', + parameters=('pokemon',) + ) + self.event_manager.register_event( + 'pokemon_caught', + parameters=( + 'pokemon', + 'cp', 'iv', 'iv_display', 'exp' + ) + ) + self.event_manager.register_event( + 'pokemon_evolved', + parameters=('pokemon', 'iv', 'cp') + ) + self.event_manager.register_event( + 'pokemon_evolve_fail', + parameters=('pokemon',) + ) + self.event_manager.register_event('skip_evolve') + self.event_manager.register_event('threw_berry_failed', parameters=('status_code',)) + self.event_manager.register_event('vip_pokemon') + + + # level up stuff + self.event_manager.register_event( + 'level_up', + parameters=( + 'previous_level', + 'current_level' + ) + ) + self.event_manager.register_event( + 'level_up_reward', + parameters=('items',) + ) + + # lucky egg + self.event_manager.register_event( + 'used_lucky_egg', + parameters=('amount_left',) + ) + self.event_manager.register_event('lucky_egg_error') + + # softban + self.event_manager.register_event('softban') + self.event_manager.register_event('softban_fix') + self.event_manager.register_event('softban_fix_done') + + # egg incubating + self.event_manager.register_event( + 'incubate_try', + parameters=( + 'incubator_id', + 'egg_id' + ) + ) + self.event_manager.register_event( + 'incubate', + parameters=('distance_in_km',) + ) + self.event_manager.register_event( + 'next_egg_incubates', + parameters=('distance_in_km',) + ) + self.event_manager.register_event('incubator_already_used') + self.event_manager.register_event('egg_already_incubating') + self.event_manager.register_event( + 'egg_hatched', + parameters=( + 'pokemon', + 'cp', 'iv', 'exp', 'stardust', 'candy' + ) + ) + + # discard item + self.event_manager.register_event( + 'item_discarded', + parameters=( + 'amount', 'item', 'maximum' + ) + ) + self.event_manager.register_event( + 'item_discard_fail', + parameters=('item',) + ) + + # inventory + self.event_manager.register_event('inventory_full') + + # release + self.event_manager.register_event( + 'keep_best_release', + parameters=( + 'amount', 'pokemon', 'criteria' + ) + ) + self.event_manager.register_event( + 'future_pokemon_release', + parameters=( + 'pokemon', 'cp', 'iv', 'below_iv', 'below_cp', 'cp_iv_logic' + ) + ) + self.event_manager.register_event( + 'pokemon_release', + parameters=('pokemon', 'cp', 'iv') + ) + + # polyline walker + self.event_manager.register_event( + 'polyline_request', + parameters=('url',) + ) + + # cluster + self.event_manager.register_event( + 'found_cluster', + parameters=( + 'num_points', 'forts', 'radius', 'distance' + ) + ) + self.event_manager.register_event( + 'arrived_at_cluster', + parameters=( + 'forts', 'radius' + ) + ) + + # rename + self.event_manager.register_event( + 'rename_pokemon', + parameters=( + 'old_name', 'current_name' + ) + ) + self.event_manager.register_event( + 'pokemon_nickname_invalid', + parameters=('nickname',) + ) + self.event_manager.register_event('unset_pokemon_nickname') + + def tick(self): + self.cell = self.get_meta_cell() + self.tick_count += 1 + + # Check if session token has expired + self.check_session(self.position[0:2]) + + for worker in self.workers: + if worker.work() == WorkerResult.RUNNING: + return + + def get_meta_cell(self): + location = self.position[0:2] + cells = self.find_close_cells(*location) + + # Combine all cells into a single dict of the items we care about. + forts = [] + wild_pokemons = [] + catchable_pokemons = [] + for cell in cells: + if "forts" in cell and len(cell["forts"]): + forts += cell["forts"] + if "wild_pokemons" in cell and len(cell["wild_pokemons"]): + wild_pokemons += cell["wild_pokemons"] + if "catchable_pokemons" in cell and len(cell["catchable_pokemons"]): + catchable_pokemons += cell["catchable_pokemons"] + + # If there are forts present in the cells sent from the server or we don't yet have any cell data, return all data retrieved + if len(forts) > 1 or not self.cell: + return { + "forts": forts, + "wild_pokemons": wild_pokemons, + "catchable_pokemons": catchable_pokemons + } + # If there are no forts present in the data from the server, keep our existing fort data and only update the pokemon cells. + else: + return { + "forts": self.cell["forts"], + "wild_pokemons": wild_pokemons, + "catchable_pokemons": catchable_pokemons + } + + def update_web_location(self, cells=[], lat=None, lng=None, alt=None): + # we can call the function with no arguments and still get the position + # and map_cells + if lat is None: + lat = self.api._position_lat + if lng is None: + lng = self.api._position_lng + if alt is None: + alt = 0 + + if cells == []: + location = self.position[0:2] + cells = self.find_close_cells(*location) + + # insert detail info about gym to fort + for cell in cells: + if 'forts' in cell: + for fort in cell['forts']: + if fort.get('type') != 1: + response_gym_details = self.api.get_gym_details( + gym_id=fort.get('id'), + player_latitude=lng, + player_longitude=lat, + gym_latitude=fort.get('latitude'), + gym_longitude=fort.get('longitude') + ) + fort['gym_details'] = response_gym_details.get( + 'responses', {} + ).get('GET_GYM_DETAILS', None) + + user_data_cells = "data/cells-%s.json" % self.config.username + with open(user_data_cells, 'w') as outfile: + json.dump(cells, outfile) + + user_web_location = os.path.join( + 'web', 'location-%s.json' % self.config.username + ) + # alt is unused atm but makes using *location easier + try: + with open(user_web_location, 'w') as outfile: + json.dump({ + 'lat': lat, + 'lng': lng, + 'alt': alt, + 'cells': cells + }, outfile) + except IOError as e: + self.logger.info('[x] Error while opening location file: %s' % e) + + user_data_lastlocation = os.path.join( + 'data', 'last-location-%s.json' % self.config.username + ) + try: + with open(user_data_lastlocation, 'w') as outfile: + json.dump({'lat': lat, 'lng': lng, 'start_position': self.start_position}, outfile) + except IOError as e: + self.logger.info('[x] Error while opening location file: %s' % e) + + def find_close_cells(self, lat, lng): + cellid = get_cell_ids(lat, lng) + timestamp = [0, ] * len(cellid) + response_dict = self.get_map_objects(lat, lng, timestamp, cellid) + map_objects = response_dict.get( + 'responses', {} + ).get('GET_MAP_OBJECTS', {}) + status = map_objects.get('status', None) + + map_cells = [] + if status and status == 1: + map_cells = map_objects['map_cells'] + position = (lat, lng, 0) + map_cells.sort( + key=lambda x: distance( + lat, + lng, + x['forts'][0]['latitude'], + x['forts'][0]['longitude']) if x.get('forts', []) else 1e6 + ) + return map_cells def _setup_logging(self): - self.log = logging.getLogger(__name__) # log settings # log format - logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s [%(module)10s] [%(levelname)5s] %(message)s') if self.config.debug: + log_level = logging.DEBUG logging.getLogger("requests").setLevel(logging.DEBUG) + logging.getLogger("websocket").setLevel(logging.DEBUG) + logging.getLogger("socketio").setLevel(logging.DEBUG) + logging.getLogger("engineio").setLevel(logging.DEBUG) + logging.getLogger("socketIO-client").setLevel(logging.DEBUG) logging.getLogger("pgoapi").setLevel(logging.DEBUG) logging.getLogger("rpc_api").setLevel(logging.DEBUG) else: + log_level = logging.ERROR logging.getLogger("requests").setLevel(logging.ERROR) + logging.getLogger("websocket").setLevel(logging.ERROR) + logging.getLogger("socketio").setLevel(logging.ERROR) + logging.getLogger("engineio").setLevel(logging.ERROR) + logging.getLogger("socketIO-client").setLevel(logging.ERROR) logging.getLogger("pgoapi").setLevel(logging.ERROR) logging.getLogger("rpc_api").setLevel(logging.ERROR) - def _setup_api(self): - # instantiate pgoapi - self.api = PGoApi() + logging.basicConfig( + level=log_level, + format='%(asctime)s [%(name)10s] [%(levelname)s] %(message)s' + ) + def check_session(self, position): + # Check session expiry + if self.api._auth_provider and self.api._auth_provider._ticket_expire: + + # prevent crash if return not numeric value + if not self.is_numeric(self.api._auth_provider._ticket_expire): + self.logger.info("Ticket expired value is not numeric", 'yellow') + return + + remaining_time = \ + self.api._auth_provider._ticket_expire / 1000 - time.time() - # check if the release_config file exists + if remaining_time < 60: + self.logger.info("Session stale, re-logging in", 'yellow') + position = self.position + self.api = ApiWrapper() + self.position = position + self.login() + + @staticmethod + def is_numeric(s): try: - with open('release_config.json') as file: - pass - except: - # the file does not exist, warn the user and exit. - logger.log('[#] IMPORTANT: Rename and configure release_config.json.example for your Pokemon release logic first!', 'red') - exit(0) + float(s) + return True + except ValueError: + return False + + def login(self): + self.event_manager.emit( + 'login_started', + sender=self, + level='info', + formatted="Login procedure started." + ) + lat, lng = self.position[0:2] + self.api.set_position(lat, lng, 0) + + while not self.api.login( + self.config.auth_service, + str(self.config.username), + str(self.config.password)): + + self.event_manager.emit( + 'login_failed', + sender=self, + level='info', + formatted="Login error, server busy. Waiting 10 seconds to try again." + ) + time.sleep(10) + + self.event_manager.emit( + 'login_successful', + sender=self, + level='info', + formatted="Login successful." + ) + + def _setup_api(self): + # instantiate pgoapi + self.api = ApiWrapper() # provide player position on the earth self._set_starting_position() - if not self.api.login(self.config.auth_service, - str(self.config.username), - str(self.config.password)): - logger.log('Login Error, server busy', 'red') - exit(0) - + self.login() # chain subrequests (methods) into one RPC call + self._print_character_info() + + self.logger.info('') + self.update_inventory() + # send empty map_cells and then our position + self.update_web_location() + + def _print_character_info(self): # get player profile call # ---------------------- - self.api.get_player() - - response_dict = self.api.call() - #print('Response dictionary: \n\r{}'.format(json.dumps(response_dict, indent=2))) + response_dict = self.api.get_player() + # print('Response dictionary: \n\r{}'.format(json.dumps(response_dict, indent=2))) currency_1 = "0" currency_2 = "0" - player = response_dict['responses']['GET_PLAYER']['player_data'] + if response_dict: + self._player = response_dict['responses']['GET_PLAYER']['player_data'] + player = self._player + else: + self.logger.info( + "The API didn't return player info, servers are unstable - " + "retrying.", 'red' + ) + sleep(5) + self._print_character_info() # @@@ TODO: Convert this to d/m/Y H:M:S creation_date = datetime.datetime.fromtimestamp( player['creation_timestamp_ms'] / 1e3) + creation_date = creation_date.strftime("%Y/%m/%d %H:%M:%S") pokecoins = '0' stardust = '0' - balls_stock = self.pokeball_inventory() + items_stock = self.current_inventory() if 'amount' in player['currencies'][0]: pokecoins = player['currencies'][0]['amount'] if 'amount' in player['currencies'][1]: stardust = player['currencies'][1]['amount'] - - logger.log('[#] Username: {username}'.format(**player)) - logger.log('[#] Acccount Creation: {}'.format(creation_date)) - logger.log('[#] Bag Storage: {}/{}'.format( - self.get_inventory_count('item'), player['max_item_storage'])) - logger.log('[#] Pokemon Storage: {}/{}'.format( - self.get_inventory_count('pokemon'), player[ - 'max_pokemon_storage'])) - logger.log('[#] Stardust: {}'.format(stardust)) - logger.log('[#] Pokecoins: {}'.format(pokecoins)) - logger.log('[#] PokeBalls: ' + str(balls_stock[1])) - logger.log('[#] GreatBalls: ' + str(balls_stock[2])) - logger.log('[#] UltraBalls: ' + str(balls_stock[3])) - + self.logger.info('') + self.logger.info('--- {username} ---'.format(**player)) self.get_player_info() - - if self.config.initial_transfer: - worker = InitialTransferWorker(self) - worker.work() - - logger.log('[#]') - self.update_inventory() - - def catch_pokemon(self, pokemon): - worker = PokemonCatchWorker(pokemon, self) - return_value = worker.work() - - if return_value == PokemonCatchWorker.BAG_FULL: - worker = InitialTransferWorker(self) - worker.work() - - return return_value - - def drop_item(self, item_id, count): - self.api.recycle_inventory_item(item_id=item_id, count=count) - inventory_req = self.api.call() - - # Example of good request response - #{'responses': {'RECYCLE_INVENTORY_ITEM': {'result': 1, 'new_count': 46}}, 'status_code': 1, 'auth_ticket': {'expire_timestamp_ms': 1469306228058L, 'start': '/HycFyfrT4t2yB2Ij+yoi+on778aymMgxY6RQgvrGAfQlNzRuIjpcnDd5dAxmfoTqDQrbz1m2dGqAIhJ+eFapg==', 'end': 'f5NOZ95a843tgzprJo4W7Q=='}, 'request_id': 8145806132888207460L} - return inventory_req + self.logger.info( + 'Pokemon Bag: {}/{}'.format( + self.get_inventory_count('pokemon'), + player['max_pokemon_storage'] + ) + ) + self.logger.info( + 'Items: {}/{}'.format( + self.get_inventory_count('item'), + player['max_item_storage'] + ) + ) + self.logger.info( + 'Stardust: {}'.format(stardust) + + ' | Pokecoins: {}'.format(pokecoins) + ) + # Items Output + self.logger.info( + 'PokeBalls: ' + str(items_stock[1]) + + ' | GreatBalls: ' + str(items_stock[2]) + + ' | UltraBalls: ' + str(items_stock[3])) + + self.logger.info( + 'RazzBerries: ' + str(items_stock[701]) + + ' | BlukBerries: ' + str(items_stock[702]) + + ' | NanabBerries: ' + str(items_stock[703])) + + self.logger.info( + 'LuckyEgg: ' + str(items_stock[301]) + + ' | Incubator: ' + str(items_stock[902]) + + ' | TroyDisk: ' + str(items_stock[501])) + + self.logger.info( + 'Potion: ' + str(items_stock[101]) + + ' | SuperPotion: ' + str(items_stock[102]) + + ' | HyperPotion: ' + str(items_stock[103])) + + self.logger.info( + 'Incense: ' + str(items_stock[401]) + + ' | IncenseSpicy: ' + str(items_stock[402]) + + ' | IncenseCool: ' + str(items_stock[403])) + + self.logger.info( + 'Revive: ' + str(items_stock[201]) + + ' | MaxRevive: ' + str(items_stock[202])) + + self.logger.info('') + + def use_lucky_egg(self): + return self.api.use_item_xp_boost(item_id=301) + + def get_inventory(self): + if self.latest_inventory is None: + self.latest_inventory = self.api.get_inventory() + return self.latest_inventory def update_inventory(self): - self.api.get_inventory() - response = self.api.call() + response = self.get_inventory() self.inventory = list() - if 'responses' in response: - if 'GET_INVENTORY' in response['responses']: - if 'inventory_delta' in response['responses']['GET_INVENTORY']: - if 'inventory_items' in response['responses'][ - 'GET_INVENTORY']['inventory_delta']: - for item in response['responses']['GET_INVENTORY'][ - 'inventory_delta']['inventory_items']: - if not 'inventory_item_data' in item: - continue - if not 'item' in item['inventory_item_data']: - continue - if not 'item_id' in item['inventory_item_data'][ - 'item']: - continue - if not 'count' in item['inventory_item_data'][ - 'item']: - continue - self.inventory.append(item['inventory_item_data'][ - 'item']) - - def pokeball_inventory(self): - self.api.get_player().get_inventory() - - inventory_req = self.api.call() + inventory_items = response.get('responses', {}).get('GET_INVENTORY', {}).get( + 'inventory_delta', {}).get('inventory_items', {}) + if inventory_items: + for item in inventory_items: + item_info = item.get('inventory_item_data', {}).get('item', {}) + if {"item_id", "count"}.issubset(set(item_info.keys())): + self.inventory.append(item['inventory_item_data']['item']) + + def current_inventory(self): + inventory_req = self.get_inventory() inventory_dict = inventory_req['responses']['GET_INVENTORY'][ 'inventory_delta']['inventory_items'] - user_web_inventory = 'web/inventory-%s.json' % (self.config.username) + user_web_inventory = 'web/inventory-%s.json' % self.config.username + with open(user_web_inventory, 'w') as outfile: json.dump(inventory_dict, outfile) - # get player balls stock + # get player items stock # ---------------------- - balls_stock = {1: 0, 2: 0, 3: 0, 4: 0} + items_stock = {x.value: 0 for x in list(Item)} for item in inventory_dict: - try: - # print(item['inventory_item_data']['item']) - item_id = item['inventory_item_data']['item']['item_id'] - item_count = item['inventory_item_data']['item']['count'] - - if item_id == Item.ITEM_POKE_BALL.value: - # print('Poke Ball count: ' + str(item_count)) - balls_stock[1] = item_count - if item_id == Item.ITEM_GREAT_BALL.value: - # print('Great Ball count: ' + str(item_count)) - balls_stock[2] = item_count - if item_id == Item.ITEM_ULTRA_BALL.value: - # print('Ultra Ball count: ' + str(item_count)) - balls_stock[3] = item_count - except: - continue - return balls_stock + item_dict = item.get('inventory_item_data', {}).get('item', {}) + item_count = item_dict.get('count') + item_id = item_dict.get('item_id') - def item_inventory_count(self, id): - self.api.get_player().get_inventory() + if item_count and item_id: + if item_id in items_stock: + items_stock[item_id] = item_count + return items_stock - inventory_req = self.api.call() + def item_inventory_count(self, id): + inventory_req = self.get_inventory() inventory_dict = inventory_req['responses'][ 'GET_INVENTORY']['inventory_delta']['inventory_items'] + if id == 'all': + return self._all_items_inventory_count(inventory_dict) + else: + return self._item_inventory_count_per_id(id, inventory_dict) + + def _item_inventory_count_per_id(self, id, inventory_dict): item_count = 0 for item in inventory_dict: - try: - if item['inventory_item_data']['item']['item_id'] == int(id): - item_count = item[ - 'inventory_item_data']['item']['count'] - except: - continue - return item_count + item_dict = item.get('inventory_item_data', {}).get('item', {}) + item_id = item_dict.get('item_id', False) + item_count = item_dict.get('count', False) + if item_id == int(id) and item_count: + return item_count + return 0 + + def _all_items_inventory_count(self, inventory_dict): + item_count_dict = {} + + for item in inventory_dict: + item_dict = item.get('inventory_item_data', {}).get('item', {}) + item_id = item_dict.get('item_id', False) + item_count = item_dict.get('count', False) + if item_id and item_count: + item_count_dict[item_id] = item_count + + return item_count_dict def _set_starting_position(self): + self.event_manager.emit( + 'set_start_location', + sender=self, + level='info', + formatted='Setting start location.' + ) + + has_position = False + if self.config.test: # TODO: Add unit tests return if self.config.location: + location_str = self.config.location + location = self.get_pos_by_name(location_str.replace(" ", "")) + msg = "Location found: {location} {position}" + self.event_manager.emit( + 'location_found', + sender=self, + level='info', + formatted=msg, + data={ + 'location': location_str, + 'position': location + } + ) + + self.api.set_position(*location) + + self.event_manager.emit( + 'position_update', + sender=self, + level='info', + formatted="Now at {current_position}", + data={ + 'current_position': self.position, + 'last_position': '', + 'distance': '', + 'distance_unit': '' + } + ) + + self.start_position = self.position + + has_position = True + + if self.config.location_cache: try: - location_str = str(self.config.location) - location = (self._get_pos_by_name(location_str.replace(" ", ""))) - self.position = location - self.api.set_position(*self.position) - logger.log('') - logger.log(u'[x] Address found: {}'.format(self.config.location.decode( - 'utf-8'))) - logger.log('[x] Position in-game set as: {}'.format(self.position)) - logger.log('') - return - except: - logger.log('[x] The location given using -l could not be parsed. Checking for a cached location.') - pass - - if self.config.location_cache and not self.config.location: - try: - # # save location flag used to pull the last known location from # the location.json + self.event_manager.emit( + 'load_cached_location', + sender=self, + level='debug', + formatted='Loading cached location...' + ) with open('data/last-location-%s.json' % - (self.config.username)) as f: + self.config.username) as f: location_json = json.load(f) - - self.position = (location_json['lat'], - location_json['lng'], 0.0) - self.api.set_position(*self.position) - - logger.log('') - logger.log( - '[x] Last location flag used. Overriding passed in location') - logger.log( - '[x] Last in-game location was set as: {}'.format( - self.position)) - logger.log('') - - return - except: - if not self.config.location: + location = ( + location_json['lat'], + location_json['lng'], + 0.0 + ) + + # If location has been set in config, only use cache if starting position has not differed + if has_position and 'start_position' in location_json: + last_start_position = tuple(location_json.get('start_position', [])) + + # Start position has to have been set on a previous run to do this check + if last_start_position and last_start_position != self.start_position: + msg = 'Going to a new place, ignoring cached location.' + self.event_manager.emit( + 'location_cache_ignored', + sender=self, + level='debug', + formatted=msg + ) + return + + self.api.set_position(*location) + self.event_manager.emit( + 'position_update', + sender=self, + level='debug', + formatted='Loaded location {current_position} from cache', + data={ + 'current_position': location, + 'last_position': '', + 'distance': '', + 'distance_unit': '' + } + ) + + has_position = True + except Exception: + if has_position is False: sys.exit( - "No cached Location. Please specify initial location.") - else: - pass - - def _get_pos_by_name(self, location_name): + "No cached Location. Please specify initial location." + ) + self.event_manager.emit( + 'location_cache_error', + sender=self, + level='debug', + formatted='Parsing cached location failed.' + ) + + def get_pos_by_name(self, location_name): # Check if the given location is already a coordinate. if ',' in location_name: - possibleCoordinates = re.findall("[-]?\d{1,3}[.]\d{6,7}", location_name) - if len(possibleCoordinates) == 2: - # 2 matches, this must be a coordinate. We'll bypass the Google geocode so we keep the exact location. - logger.log( - '[x] Coordinates found in passed in location, not geocoding.') - return (float(possibleCoordinates[0]), float(possibleCoordinates[1]), float("0.0")) + possible_coordinates = re.findall( + "[-]?\d{1,3}[.]\d{3,7}", location_name + ) + if len(possible_coordinates) == 2: + # 2 matches, this must be a coordinate. We'll bypass the Google + # geocode so we keep the exact location. + self.logger.info( + '[x] Coordinates found in passed in location, ' + 'not geocoding.' + ) + return float(possible_coordinates[0]), float(possible_coordinates[1]), float("0.0") geolocator = GoogleV3(api_key=self.config.gmapkey) loc = geolocator.geocode(location_name, timeout=10) - #self.log.info('Your given location: %s', loc.address.encode('utf-8')) - #self.log.info('lat/long/alt: %s %s %s', loc.latitude, loc.longitude, loc.altitude) - - return (loc.latitude, loc.longitude, loc.altitude) - - def _filter_ignored_pokemons(self, cell): - process_ignore = False - try: - with open("./data/catch-ignore.yml", 'r') as y: - ignores = yaml.load(y)['ignore'] - if len(ignores) > 0: - process_ignore = True - except Exception, e: - pass - - if process_ignore: - # - # remove any wild pokemon - try: - for p in cell['wild_pokemons'][:]: - pokemon_id = p['pokemon_data']['pokemon_id'] - pokemon_name = filter( - lambda x: int(x.get('Number')) == pokemon_id, - self.pokemon_list)[0]['Name'] - - if pokemon_name in ignores: - cell['wild_pokemons'].remove(p) - except KeyError: - pass - - # - # remove catchable pokemon - try: - for p in cell['catchable_pokemons'][:]: - pokemon_id = p['pokemon_id'] - pokemon_name = filter( - lambda x: int(x.get('Number')) == pokemon_id, - self.pokemon_list)[0]['Name'] - - if pokemon_name in ignores: - cell['catchable_pokemons'].remove(p) - except KeyError: - pass + return float(loc.latitude), float(loc.longitude), float(loc.altitude) def heartbeat(self): - self.api.get_player() - self.api.get_hatched_eggs() - self.api.get_inventory() - self.api.check_awarded_badges() - self.api.call() + # Remove forts that we can now spin again. + self.fort_timeouts = {id: timeout for id, timeout + in self.fort_timeouts.iteritems() + if timeout >= time.time() * 1000} + request = self.api.create_request() + request.get_player() + request.check_awarded_badges() + request.call() + self.update_web_location() # updates every tick def get_inventory_count(self, what): - self.api.get_inventory() - response_dict = self.api.call() - if 'responses' in response_dict: - if 'GET_INVENTORY' in response_dict['responses']: - if 'inventory_delta' in response_dict['responses'][ - 'GET_INVENTORY']: - if 'inventory_items' in response_dict['responses'][ - 'GET_INVENTORY']['inventory_delta']: - pokecount = 0 - itemcount = 1 - for item in response_dict['responses'][ - 'GET_INVENTORY']['inventory_delta'][ - 'inventory_items']: - #print('item {}'.format(item)) - if 'inventory_item_data' in item: - if 'pokemon_data' in item[ - 'inventory_item_data']: - pokecount = pokecount + 1 - if 'item' in item['inventory_item_data']: - if 'count' in item['inventory_item_data'][ - 'item']: - itemcount = itemcount + \ - item['inventory_item_data'][ - 'item']['count'] + response_dict = self.get_inventory() + inventory_items = response_dict.get('responses', {}).get('GET_INVENTORY', {}).get( + 'inventory_delta', {}).get('inventory_items', {}) + if inventory_items: + pokecount = 0 + itemcount = 1 + for item in inventory_items: + if 'inventory_item_data' in item: + if 'pokemon_data' in item['inventory_item_data']: + pokecount += 1 + itemcount += item['inventory_item_data'].get('item', {}).get('count', 0) if 'pokemon' in what: return pokecount if 'item' in what: @@ -426,49 +924,70 @@ def get_inventory_count(self, what): return '0' def get_player_info(self): - self.api.get_inventory() - response_dict = self.api.call() - if 'responses' in response_dict: - if 'GET_INVENTORY' in response_dict['responses']: - if 'inventory_delta' in response_dict['responses'][ - 'GET_INVENTORY']: - if 'inventory_items' in response_dict['responses'][ - 'GET_INVENTORY']['inventory_delta']: - pokecount = 0 - itemcount = 1 - for item in response_dict['responses'][ - 'GET_INVENTORY']['inventory_delta'][ - 'inventory_items']: - #print('item {}'.format(item)) - if 'inventory_item_data' in item: - if 'player_stats' in item[ - 'inventory_item_data']: - playerdata = item['inventory_item_data'][ - 'player_stats'] - - nextlvlxp = ( - int(playerdata.get('next_level_xp', 0)) - - int(playerdata.get('experience', 0))) - - if 'level' in playerdata: - logger.log( - '[#] -- Level: {level}'.format( - **playerdata)) - - if 'experience' in playerdata: - logger.log( - '[#] -- Experience: {experience}'.format( - **playerdata)) - logger.log( - '[#] -- Experience until next level: {}'.format( - nextlvlxp)) - - if 'pokemons_captured' in playerdata: - logger.log( - '[#] -- Pokemon Captured: {pokemons_captured}'.format( - **playerdata)) - - if 'poke_stop_visits' in playerdata: - logger.log( - '[#] -- Pokestops Visited: {poke_stop_visits}'.format( - **playerdata)) + response_dict = self.get_inventory() + inventory_items = response_dict.get('responses', {}).get('GET_INVENTORY', {}).get( + 'inventory_delta', {}).get('inventory_items', {}) + if inventory_items: + pokecount = 0 + itemcount = 1 + for item in inventory_items: + # print('item {}'.format(item)) + playerdata = item.get('inventory_item_data', {}).get('player_stats') + if playerdata: + nextlvlxp = (int(playerdata.get('next_level_xp', 0)) - int(playerdata.get('experience', 0))) + + if 'level' in playerdata and 'experience' in playerdata: + self.logger.info( + 'Level: {level}'.format( + **playerdata) + + ' (Next Level: {} XP)'.format( + nextlvlxp) + + ' (Total: {experience} XP)' + ''.format(**playerdata)) + + if 'pokemons_captured' in playerdata and 'poke_stop_visits' in playerdata: + self.logger.info( + 'Pokemon Captured: ' + '{pokemons_captured}'.format( + **playerdata) + + ' | Pokestops Visited: ' + '{poke_stop_visits}'.format( + **playerdata)) + + def has_space_for_loot(self): + number_of_things_gained_by_stop = 5 + enough_space = ( + self.get_inventory_count('item') < + self._player['max_item_storage'] - number_of_things_gained_by_stop + ) + + return enough_space + + def get_forts(self, order_by_distance=False): + forts = [fort + for fort in self.cell['forts'] + if 'latitude' in fort and 'type' in fort] + + if order_by_distance: + forts.sort(key=lambda x: distance( + self.position[0], + self.position[1], + x['latitude'], + x['longitude'] + )) + + return forts + + def get_map_objects(self, lat, lng, timestamp, cellid): + if time.time() - self.last_time_map_object < self.config.map_object_cache_time: + return self.last_map_object + + self.last_map_object = self.api.get_map_objects( + latitude=f2i(lat), + longitude=f2i(lng), + since_timestamp_ms=timestamp, + cell_id=cellid + ) + self.last_time_map_object = time.time() + + return self.last_map_object diff --git a/pokemongo_bot/api_wrapper.py b/pokemongo_bot/api_wrapper.py new file mode 100644 index 0000000000..324d645043 --- /dev/null +++ b/pokemongo_bot/api_wrapper.py @@ -0,0 +1,157 @@ +import time +import logging + +from pgoapi.exceptions import (ServerSideRequestThrottlingException, + NotLoggedInException, ServerBusyOrOfflineException, + NoPlayerPositionSetException, EmptySubrequestChainException, + UnexpectedResponseException) +from pgoapi.pgoapi import PGoApi, PGoApiRequest, RpcApi +from pgoapi.protos.POGOProtos.Networking.Requests_pb2 import RequestType + +from human_behaviour import sleep + +class ApiWrapper(PGoApi): + def __init__(self): + PGoApi.__init__(self) + self.useVanillaRequest = False + + def create_request(self): + RequestClass = ApiRequest + if self.useVanillaRequest: + RequestClass = PGoApiRequest + + return RequestClass( + self._api_endpoint, + self._auth_provider, + self._position_lat, + self._position_lng, + self._position_alt + ) + + def login(self, *args): + # login needs base class "create_request" + self.useVanillaRequest = True + try: + ret_value = PGoApi.login(self, *args) + finally: + # cleanup code + self.useVanillaRequest = False + return ret_value + + +class ApiRequest(PGoApiRequest): + def __init__(self, *args): + PGoApiRequest.__init__(self, *args) + self.logger = logging.getLogger(__name__) + self.request_callers = [] + self.last_api_request_time = None + self.requests_per_seconds = 2 + + def can_call(self): + if not self._req_method_list: + raise EmptySubrequestChainException() + + if (self._position_lat is None) or (self._position_lng is None) or (self._position_alt is None): + raise NoPlayerPositionSetException() + + if self._auth_provider is None or not self._auth_provider.is_login(): + self.log.info('Not logged in') + raise NotLoggedInException() + + return True + + def _call(self): + return PGoApiRequest.call(self) + + def _pop_request_callers(self): + r = self.request_callers + self.request_callers = [] + return [i.upper() for i in r] + + def is_response_valid(self, result, request_callers): + if not result or result is None or not isinstance(result, dict): + return False + + if not 'responses' in result or not 'status_code' in result: + return False + + if not isinstance(result['responses'], dict): + return False + + # the response can still programatically be valid at this point + # but still be wrong. we need to check if the server did sent what we asked it + for request_caller in request_callers: + if not request_caller in result['responses']: + return False + + return True + + def call(self, max_retry=15): + request_callers = self._pop_request_callers() + if not self.can_call(): + return False # currently this is never ran, exceptions are raised before + + request_timestamp = None + api_req_method_list = self._req_method_list + result = None + try_cnt = 0 + throttling_retry = 0 + unexpected_response_retry = 0 + while True: + request_timestamp = self.throttle_sleep() + # self._call internally clear this field, so save it + self._req_method_list = [req_method for req_method in api_req_method_list] + should_throttle_retry = False + should_unexpected_response_retry = False + try: + result = self._call() + except ServerSideRequestThrottlingException: + should_throttle_retry = True + except UnexpectedResponseException: + should_unexpected_response_retry = True + + if should_throttle_retry: + throttling_retry += 1 + if throttling_retry >= max_retry: + raise ServerSideRequestThrottlingException('Server throttled too many times') + sleep(1) # huge sleep ? + continue # skip response checking + + if should_unexpected_response_retry: + unexpected_response_retry += 1 + if unexpected_response_retry >= 5: + self.logger.warning('Server is not responding correctly to our requests. Waiting for 30 seconds to reconnect.') + sleep(30) + else: + sleep(2) + continue + + if not self.is_response_valid(result, request_callers): + try_cnt += 1 + if try_cnt > 3: + self.logger.warning('Server seems to be busy or offline - try again - {}/{}'.format(try_cnt, max_retry)) + if try_cnt >= max_retry: + raise ServerBusyOrOfflineException() + sleep(1) + else: + break + + self.last_api_request_time = request_timestamp + return result + + def __getattr__(self, func): + if func.upper() in RequestType.keys(): + self.request_callers.append(func) + return PGoApiRequest.__getattr__(self, func) + + def throttle_sleep(self): + now_milliseconds = time.time() * 1000 + required_delay_between_requests = 1000 / self.requests_per_seconds + + difference = now_milliseconds - (self.last_api_request_time if self.last_api_request_time else 0) + + if self.last_api_request_time != None and difference < required_delay_between_requests: + sleep_time = required_delay_between_requests - difference + time.sleep(sleep_time / 1000) + + return now_milliseconds diff --git a/pokemongo_bot/cell_workers/__init__.py b/pokemongo_bot/cell_workers/__init__.py index 8f6653421c..bc6638d1fd 100644 --- a/pokemongo_bot/cell_workers/__init__.py +++ b/pokemongo_bot/cell_workers/__init__.py @@ -1,7 +1,21 @@ # -*- coding: utf-8 -*- +from catch_lured_pokemon import CatchLuredPokemon +from catch_visible_pokemon import CatchVisiblePokemon +from evolve_pokemon import EvolvePokemon +from incubate_eggs import IncubateEggs +from move_to_fort import MoveToFort +from move_to_map_pokemon import MoveToMapPokemon +from nickname_pokemon import NicknamePokemon from pokemon_catch_worker import PokemonCatchWorker -from seen_fort_worker import SeenFortWorker -from move_to_fort_worker import MoveToFortWorker -from initial_transfer_worker import InitialTransferWorker -from evolve_all_worker import EvolveAllWorker +from transfer_pokemon import TransferPokemon +from recycle_items import RecycleItems +from spin_fort import SpinFort +from handle_soft_ban import HandleSoftBan +from follow_path import FollowPath +from follow_spiral import FollowSpiral +from collect_level_up_reward import CollectLevelUpReward +from base_task import BaseTask +from follow_cluster import FollowCluster +from sleep_schedule import SleepSchedule +from update_title_stats import UpdateTitleStats \ No newline at end of file diff --git a/pokemongo_bot/cell_workers/base_task.py b/pokemongo_bot/cell_workers/base_task.py new file mode 100644 index 0000000000..ac48b9a676 --- /dev/null +++ b/pokemongo_bot/cell_workers/base_task.py @@ -0,0 +1,30 @@ +import logging + + +class BaseTask(object): + + def __init__(self, bot, config): + self.bot = bot + self.config = config + self._validate_work_exists() + self.logger = logging.getLogger(type(self).__name__) + self.initialize() + + def _validate_work_exists(self): + method = getattr(self, 'work', None) + if not method or not callable(method): + raise NotImplementedError('Missing "work" method') + + def emit_event(self, event, sender=None, level='info', formatted='', data={}): + if not sender: + sender=self + self.bot.event_manager.emit( + event, + sender=sender, + level=level, + formatted=formatted, + data=data + ) + + def initialize(self): + pass diff --git a/pokemongo_bot/cell_workers/catch_lured_pokemon.py b/pokemongo_bot/cell_workers/catch_lured_pokemon.py new file mode 100644 index 0000000000..bf2d45bb4b --- /dev/null +++ b/pokemongo_bot/cell_workers/catch_lured_pokemon.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from pokemongo_bot.cell_workers.utils import fort_details +from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker +from pokemongo_bot.cell_workers.base_task import BaseTask + + +class CatchLuredPokemon(BaseTask): + def work(self): + lured_pokemon = self.get_lured_pokemon() + if lured_pokemon: + self.catch_pokemon(lured_pokemon) + + def get_lured_pokemon(self): + forts = self.bot.get_forts(order_by_distance=True) + + if len(forts) == 0: + return False + + fort = forts[0] + details = fort_details(self.bot, fort_id=fort['id'], + latitude=fort['latitude'], + longitude=fort['longitude']) + fort_name = details.get('name', 'Unknown').encode('utf8', 'replace') + + encounter_id = fort.get('lure_info', {}).get('encounter_id', None) + + if encounter_id: + result = { + 'encounter_id': encounter_id, + 'fort_id': fort['id'], + 'fort_name': fort_name, + 'latitude': fort['latitude'], + 'longitude': fort['longitude'] + } + + self.emit_event( + 'lured_pokemon_found', + formatted='Lured pokemon at fort {fort_name} ({fort_id})', + data=result + ) + return result + + return False + + def catch_pokemon(self, pokemon): + worker = PokemonCatchWorker(pokemon, self.bot) + return_value = worker.work() + + return return_value diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py new file mode 100644 index 0000000000..c9c6147c0a --- /dev/null +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -0,0 +1,48 @@ +import json + +from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker +from utils import distance + + +class CatchVisiblePokemon(BaseTask): + def work(self): + if 'catchable_pokemons' in self.bot.cell and len(self.bot.cell['catchable_pokemons']) > 0: + # Sort all by distance from current pos- eventually this should + # build graph & A* it + self.bot.cell['catchable_pokemons'].sort( + key= + lambda x: distance(self.bot.position[0], self.bot.position[1], x['latitude'], x['longitude']) + ) + + for pokemon in self.bot.cell['catchable_pokemons']: + with open(user_web_catchable, 'w') as outfile: + json.dump(pokemon, outfile) + self.emit_event( + 'catchable_pokemon', + level='debug', + data={ + 'pokemon_id': pokemon['pokemon_id'], + 'spawn_point_id': pokemon['spawn_point_id'], + 'encounter_id': pokemon['encounter_id'], + 'latitude': pokemon['latitude'], + 'longitude': pokemon['longitude'], + 'expiration_timestamp_ms': pokemon['expiration_timestamp_ms'], + } + ) + + return self.catch_pokemon(self.bot.cell['catchable_pokemons'].pop(0)) + + if 'wild_pokemons' in self.bot.cell and len(self.bot.cell['wild_pokemons']) > 0: + # Sort all by distance from current pos- eventually this should + # build graph & A* it + self.bot.cell['wild_pokemons'].sort( + key= + lambda x: distance(self.bot.position[0], self.bot.position[1], x['latitude'], x['longitude'])) + return self.catch_pokemon(self.bot.cell['wild_pokemons'].pop(0)) + + def catch_pokemon(self, pokemon): + worker = PokemonCatchWorker(pokemon, self.bot) + return_value = worker.work() + + return return_value diff --git a/pokemongo_bot/cell_workers/collect_level_up_reward.py b/pokemongo_bot/cell_workers/collect_level_up_reward.py new file mode 100644 index 0000000000..304818fe2b --- /dev/null +++ b/pokemongo_bot/cell_workers/collect_level_up_reward.py @@ -0,0 +1,74 @@ +from pokemongo_bot.cell_workers.base_task import BaseTask + + +class CollectLevelUpReward(BaseTask): + current_level = 0 + previous_level = 0 + + def initialize(self): + self.current_level = self._get_current_level() + self.previous_level = 0 + + def work(self): + self.current_level = self._get_current_level() + + # let's check level reward on bot initialization + # to be able get rewards for old bots + if self.previous_level == 0: + self._collect_level_reward() + # level up situation + elif self.current_level > self.previous_level: + self.emit_event( + 'level_up', + formatted='Level up from {previous_level} to {current_level}', + data={ + 'previous_level': self.previous_level, + 'current_level': self.current_level + } + ) + self._collect_level_reward() + + self.previous_level = self.current_level + + def _collect_level_reward(self): + response_dict = self.bot.api.level_up_rewards(level=self.current_level) + if 'status_code' in response_dict and response_dict['status_code'] == 1: + data = (response_dict + .get('responses', {}) + .get('LEVEL_UP_REWARDS', {}) + .get('items_awarded', [])) + + for item in data: + if 'item_id' in item and str(item['item_id']) in self.bot.item_list: + got_item = self.bot.item_list[str(item['item_id'])] + item['name'] = got_item + count = 'item_count' in item and item['item_count'] or 0 + + self.emit_event( + 'level_up_reward', + formatted='Received level up reward: {items}', + data={ + 'items': data + } + ) + + def _get_current_level(self): + level = 0 + response_dict = self.bot.get_inventory() + data = (response_dict + .get('responses', {}) + .get('GET_INVENTORY', {}) + .get('inventory_delta', {}) + .get('inventory_items', {})) + + for item in data: + level = (item + .get('inventory_item_data', {}) + .get('player_stats', {}) + .get('level', 0)) + + # we found a level, no need to continue iterate + if level: + break + + return level diff --git a/pokemongo_bot/cell_workers/evolve_all_worker.py b/pokemongo_bot/cell_workers/evolve_all_worker.py deleted file mode 100644 index 9c46dc7555..0000000000 --- a/pokemongo_bot/cell_workers/evolve_all_worker.py +++ /dev/null @@ -1,240 +0,0 @@ -from utils import distance, format_dist -from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot import logger -from sets import Set - -class EvolveAllWorker(object): - def __init__(self, bot): - self.api = bot.api - self.config = bot.config - self.bot = bot - # self.position = bot.position - - def work(self): - self.api.get_inventory() - response_dict = self.api.call() - cache = {} - - try: - reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) - except KeyError: - pass - else: - evolve_list = self._sort_by_cp(response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']) - if self.config.evolve_all[0] != 'all': - # filter out non-listed pokemons - evolve_list = [x for x in evolve_list if str(x[1]) in self.config.evolve_all] - - ## enable to limit number of pokemons to evolve. Useful for testing. - # nn = 1 - # if len(evolve_list) > nn: - # evolve_list = evolve_list[:nn] - ## - - id_list1 = self.count_pokemon_inventory() - for pokemon in evolve_list: - try: - self._execute_pokemon_evolve(pokemon, cache) - except: - pass - id_list2 = self.count_pokemon_inventory() - release_cand_list_ids = list(Set(id_list2) - Set(id_list1)) - - if release_cand_list_ids: - print('[#] Evolved {} pokemons! Checking if any of them needs to be released ...'.format( - len(release_cand_list_ids) - )) - self._release_evolved(release_cand_list_ids) - - def _release_evolved(self, release_cand_list_ids): - self.api.get_inventory() - response_dict = self.api.call() - cache = {} - - try: - reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) - except KeyError: - pass - else: - release_cand_list = self._sort_by_cp(response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']) - release_cand_list = [x for x in release_cand_list if x[0] in release_cand_list_ids] - - ## at this point release_cand_list contains evolved pokemons data - for cand in release_cand_list: - pokemon_id = cand[0] - pokemon_name = cand[1] - pokemon_cp = cand[2] - pokemon_potential = cand[3] - - if self.should_release_pokemon(pokemon_name, pokemon_cp, pokemon_potential): - # Transfering Pokemon - self.transfer_pokemon(pokemon_id) - logger.log( - '[#] {} has been exchanged for candy!'.format(pokemon_name), 'green') - - def _sort_by_cp(self, inventory_items): - pokemons = [] - for item in inventory_items: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data"], item) - except KeyError: - pass - else: - try: - pokemon = item['inventory_item_data']['pokemon_data'] - pokemon_num = int(pokemon['pokemon_id']) - 1 - pokemon_name = self.bot.pokemon_list[int(pokemon_num)]['Name'] - pokemons.append([ - pokemon['id'], - pokemon_name, - pokemon['cp'], - self._compute_iv(pokemon) - ]) - except: - pass - - pokemons.sort(key=lambda x: x[2], reverse=True) - return pokemons - - def _execute_pokemon_evolve(self, pokemon, cache): - pokemon_id = pokemon[0] - pokemon_name = pokemon[1] - pokemon_cp = pokemon[2] - - if pokemon_name in cache: - return - - self.api.evolve_pokemon(pokemon_id=pokemon_id) - response_dict = self.api.call() - status = response_dict['responses']['EVOLVE_POKEMON']['result'] - if status == 1: - print('[#] Successfully evolved {} with {} cp!'.format( - pokemon_name, pokemon_cp - )) - else: - # cache pokemons we can't evolve. Less server calls - cache[pokemon_name] = 1 - sleep(5.7) - - # TODO: move to utils. These methods are shared with other workers. - def transfer_pokemon(self, pid): - self.api.release_pokemon(pokemon_id=pid) - response_dict = self.api.call() - - def count_pokemon_inventory(self): - self.api.get_inventory() - response_dict = self.api.call() - id_list = [] - return self.counting_pokemon(response_dict, id_list) - - def counting_pokemon(self, response_dict, id_list): - try: - reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) - except KeyError: - pass - else: - for item in response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data"], item) - except KeyError: - pass - else: - pokemon = item['inventory_item_data']['pokemon_data'] - if pokemon.get('is_egg', False): - continue - id_list.append(pokemon['id']) - - return id_list - - def should_release_pokemon(self, pokemon_name, cp, iv): - if self._check_always_capture_exception_for(pokemon_name): - return False - else: - release_config = self._get_release_config_for(pokemon_name) - cp_iv_logic = release_config.get('cp_iv_logic') - if not cp_iv_logic: - cp_iv_logic = self._get_release_config_for('any').get('cp_iv_logic', 'and') - - release_results = { - 'cp': False, - 'iv': False, - } - - if 'release_under_cp' in release_config: - min_cp = release_config['release_under_cp'] - if cp < min_cp: - release_results['cp'] = True - - if 'release_under_iv' in release_config: - min_iv = release_config['release_under_iv'] - if iv < min_iv: - release_results['iv'] = True - - if release_config.get('always_release'): - return True - - logic_to_function = { - 'or': lambda x, y: x or y, - 'and': lambda x, y: x and y - } - - #logger.log( - # "[x] Release config for {}: CP {} {} IV {}".format( - # pokemon_name, - # min_cp, - # cp_iv_logic, - # min_iv - # ), 'yellow' - #) - - return logic_to_function[cp_iv_logic](*release_results.values()) - - def _get_release_config_for(self, pokemon): - release_config = self.config.release_config.get(pokemon) - if not release_config: - release_config = self.config.release_config['any'] - return release_config - - def _get_exceptions(self): - exceptions = self.config.release_config.get('exceptions') - if not exceptions: - return None - return exceptions - - def _get_always_capture_list(self): - exceptions = self._get_exceptions() - if not exceptions: - return [] - always_capture_list = exceptions['always_capture'] - if not always_capture_list: - return [] - return always_capture_list - - def _check_always_capture_exception_for(self, pokemon_name): - always_capture_list = self._get_always_capture_list() - if not always_capture_list: - return False - else: - for pokemon in always_capture_list: - if pokemon_name == str(pokemon): - return True - return False - - # TODO: should also go to util and refactor in catch worker - def _compute_iv(self, pokemon): - total_IV = 0.0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - - for individual_stat in iv_stats: - try: - total_IV += pokemon[individual_stat] - except: - pokemon[individual_stat] = 0 - continue - pokemon_potential = round((total_IV / 45.0), 2) - return pokemon_potential diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py new file mode 100644 index 0000000000..911d6a1f67 --- /dev/null +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -0,0 +1,168 @@ +from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.item_list import Item +from pokemongo_bot.cell_workers.base_task import BaseTask + + +class EvolvePokemon(BaseTask): + + def initialize(self): + self.api = self.bot.api + self.evolve_all = self.config.get('evolve_all', []) + self.evolve_speed = self.config.get('evolve_speed', 2) + self.first_evolve_by = self.config.get('first_evolve_by', 'cp') + self.evolve_above_cp = self.config.get('evolve_above_cp', 500) + self.evolve_above_iv = self.config.get('evolve_above_iv', 0.8) + self.cp_iv_logic = self.config.get('logic', 'or') + self.use_lucky_egg = self.config.get('use_lucky_egg', False) + self._validate_config() + + def _validate_config(self): + if isinstance(self.evolve_all, basestring): + self.evolve_all = [str(pokemon_name).strip() for pokemon_name in self.evolve_all.split(',')] + + def work(self): + if not self._should_run(): + return + + response_dict = self.api.get_inventory() + inventory_items = response_dict.get('responses', {}).get('GET_INVENTORY', {}).get('inventory_delta', {}).get( + 'inventory_items', {}) + + evolve_list = self._sort_and_filter(inventory_items) + + if self.evolve_all[0] != 'all': + # filter out non-listed pokemons + evolve_list = filter(lambda x: x["name"] in self.evolve_all, evolve_list) + + cache = {} + candy_list = self._get_candy_list(inventory_items) + for pokemon in evolve_list: + if self._can_evolve(pokemon, candy_list, cache): + self._execute_pokemon_evolve(pokemon, candy_list, cache) + + def _should_run(self): + if not self.evolve_all or self.evolve_all[0] == 'none': + return False + + # Evolve all is used - Use Lucky egg only at the first tick + if self.bot.tick_count is not 1 or not self.use_lucky_egg: + return True + + lucky_egg_count = self.bot.item_inventory_count(Item.ITEM_LUCKY_EGG.value) + + # Make sure the user has a lucky egg and skip if not + if lucky_egg_count > 0: + response_dict_lucky_egg = self.bot.use_lucky_egg() + if response_dict_lucky_egg: + result = response_dict_lucky_egg.get('responses', {}).get('USE_ITEM_XP_BOOST', {}).get('result', 0) + if result is 1: # Request success + self.emit_event( + 'used_lucky_egg', + formmated='Used lucky egg ({amount_left} left).', + data={ + 'amount_left': lucky_egg_count - 1 + } + ) + return True + else: + self.emit_event( + 'lucky_egg_error', + level='error', + formatted='Failed to use lucky egg!' + ) + return False + else: + # Skipping evolve so they aren't wasted + self.emit_event( + 'skip_evolve', + formatted='Skipping evolve because has no lucky egg.' + ) + return False + + def _get_candy_list(self, inventory_items): + candies = {} + for item in inventory_items: + candy = item.get('inventory_item_data', {}).get('candy', {}) + family_id = candy.get('family_id', 0) + amount = candy.get('candy', 0) + if family_id > 0 and amount > 0: + family = self.bot.pokemon_list[family_id - 1]['Name'] + " candies" + candies[family] = amount + + return candies + + def _sort_and_filter(self, inventory_items): + pokemons = [] + logic_to_function = { + 'or': lambda pokemon: pokemon["cp"] >= self.evolve_above_cp or pokemon["iv"] >= self.evolve_above_iv, + 'and': lambda pokemon: pokemon["cp"] >= self.evolve_above_cp and pokemon["iv"] >= self.evolve_above_iv + } + for item in inventory_items: + pokemon = item.get('inventory_item_data', {}).get('pokemon_data', {}) + pokemon_num = int(pokemon.get('pokemon_id', 0)) - 1 + next_evol = self.bot.pokemon_list[pokemon_num].get('Next Evolution Requirements', {}) + pokemon = { + 'id': pokemon.get('id', 0), + 'num': pokemon_num, + 'name': self.bot.pokemon_list[pokemon_num]['Name'], + 'cp': pokemon.get('cp', 0), + 'iv': self._compute_iv(pokemon), + 'candies_family': next_evol.get('Name', ""), + 'candies_amount': next_evol.get('Amount', 0) + } + if pokemon["id"] > 0 and pokemon["candies_amount"] > 0 and (logic_to_function[self.cp_iv_logic](pokemon)): + pokemons.append(pokemon) + + if self.first_evolve_by == "cp": + pokemons.sort(key=lambda x: (x['num'], x["cp"], x["iv"]), reverse=True) + else: + pokemons.sort(key=lambda x: (x['num'], x["iv"], x["cp"]), reverse=True) + + return pokemons + + def _can_evolve(self, pokemon, candy_list, cache): + + if pokemon["name"] in cache: + return False + + family = pokemon["candies_family"] + amount = pokemon["candies_amount"] + if family in candy_list and candy_list[family] >= amount: + return True + else: + cache[pokemon["name"]] = 1 + return False + + def _execute_pokemon_evolve(self, pokemon, candy_list, cache): + pokemon_id = pokemon["id"] + pokemon_name = pokemon["name"] + pokemon_cp = pokemon["cp"] + pokemon_iv = pokemon["iv"] + + if pokemon_name in cache: + return False + + response_dict = self.api.evolve_pokemon(pokemon_id=pokemon_id) + if response_dict.get('responses', {}).get('EVOLVE_POKEMON', {}).get('result', 0) == 1: + self.emit_event( + 'pokemon_evolved', + formatted="Successfully evolved {pokemon} with CP {cp} and IV {iv}!", + data={ + 'pokemon': pokemon_name, + 'iv': pokemon_iv, + 'cp': pokemon_cp + } + ) + candy_list[pokemon["candies_family"]] -= pokemon["candies_amount"] + sleep(self.evolve_speed) + return True + else: + # cache pokemons we can't evolve. Less server calls + cache[pokemon_name] = 1 + sleep(0.7) + return False + + def _compute_iv(self, pokemon): + total_iv = pokemon.get("individual_attack", 0) + pokemon.get("individual_stamina", 0) + pokemon.get( + "individual_defense", 0) + return round((total_iv / 45.0), 2) diff --git a/pokemongo_bot/cell_workers/follow_cluster.py b/pokemongo_bot/cell_workers/follow_cluster.py new file mode 100644 index 0000000000..02d3880a7e --- /dev/null +++ b/pokemongo_bot/cell_workers/follow_cluster.py @@ -0,0 +1,85 @@ +from pokemongo_bot.step_walker import StepWalker +from pokemongo_bot.cell_workers.utils import distance +from pokemongo_bot.cell_workers.utils import find_biggest_cluster +from pokemongo_bot.cell_workers.base_task import BaseTask + +class FollowCluster(BaseTask): + + def initialize(self): + self.is_at_destination = False + self.announced = False + self.dest = None + self._process_config() + + def _process_config(self): + self.lured = self.config.get("lured", True) + self.radius = self.config.get("radius", 50) + + def work(self): + forts = self.bot.get_forts() + log_lure_avail_str = '' + log_lured_str = '' + if self.lured: + log_lured_str = 'lured ' + lured_forts = [x for x in forts if 'lure_info' in x] + if len(lured_forts) > 0: + self.dest = find_biggest_cluster(self.radius, lured_forts, 'lure_info') + else: + log_lure_avail_str = 'No lured pokestops in vicinity. Search for normal ones instead. ' + self.dest = find_biggest_cluster(self.radius, forts) + else: + self.dest = find_biggest_cluster(self.radius, forts) + + if self.dest is not None: + + lat = self.dest['latitude'] + lng = self.dest['longitude'] + cnt = self.dest['num_points'] + + if not self.is_at_destination: + msg = log_lure_avail_str + ( + "Move to destiny {num_points}. {forts} " + "pokestops will be in range of {radius}. Walking {distance}m." + ) + self.emit_event( + 'found_cluster', + formatted=msg, + data={ + 'num_points': cnt, + 'forts': log_lured_str, + 'radius': str(self.radius), + 'distance': str(distance(self.bot.position[0], self.bot.position[1], lat, lng)) + } + ) + + self.announced = False + + if self.bot.config.walk > 0: + step_walker = StepWalker( + self.bot, + self.bot.config.walk, + lat, + lng + ) + + self.is_at_destination = False + if step_walker.step(): + self.is_at_destination = True + else: + self.bot.api.set_position(lat, lng) + + elif not self.announced: + self.emit_event( + 'arrived_at_cluster', + formatted="Arrived at cluster. {forts} are in a range of {radius}m radius.", + data={ + 'forts': str(cnt), + 'radius': self.radius + } + ) + self.announced = True + else: + lat = self.bot.position[0] + lng = self.bot.position[1] + + return [lat, lng] diff --git a/pokemongo_bot/cell_workers/follow_path.py b/pokemongo_bot/cell_workers/follow_path.py new file mode 100644 index 0000000000..04eb817593 --- /dev/null +++ b/pokemongo_bot/cell_workers/follow_path.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +import gpxpy +import gpxpy.gpx +import json +from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.cell_workers.utils import distance, i2f, format_dist +from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.step_walker import StepWalker +from pgoapi.utilities import f2i + + +class FollowPath(BaseTask): + def initialize(self): + self.ptr = 0 + self._process_config() + self.points = self.load_path() + + def _process_config(self): + self.path_file = self.config.get("path_file", None) + self.path_mode = self.config.get("path_mode", "linear") + + def load_path(self): + if self.path_file is None: + raise RuntimeError('You need to specify a path file (json or gpx)') + + if self.path_file.endswith('.json'): + return self.load_json() + elif self.path_file.endswith('.gpx'): + return self.load_gpx() + + def load_json(self): + with open(self.path_file) as data_file: + points=json.load(data_file) + # Replace Verbal Location with lat&lng. + for index, point in enumerate(points): + point_tuple = self.bot.get_pos_by_name(point['location']) + self.emit_event( + 'location_found', + level='debug', + formatted="Location found: {location} {position}", + data={ + 'location': point, + 'position': point_tuple + } + ) + points[index] = self.lat_lng_tuple_to_dict(point_tuple) + return points + + def lat_lng_tuple_to_dict(self, tpl): + return {'lat': tpl[0], 'lng': tpl[1]} + + def load_gpx(self): + gpx_file = open(self.path_file, 'r') + gpx = gpxpy.parse(gpx_file) + + if len(gpx.tracks) == 0: + raise RuntimeError('GPX file does not cotain a track') + + points = [] + track = gpx.tracks[0] + for segment in track.segments: + for point in segment.points: + points.append({"lat": point.latitude, "lng": point.longitude}) + + return points + + def work(self): + point = self.points[self.ptr] + lat = float(point['lat']) + lng = float(point['lng']) + + if self.bot.config.walk > 0: + step_walker = StepWalker( + self.bot, + self.bot.config.walk, + lat, + lng + ) + + is_at_destination = False + if step_walker.step(): + is_at_destination = True + + else: + self.bot.api.set_position(lat, lng) + + dist = distance( + self.bot.api._position_lat, + self.bot.api._position_lng, + lat, + lng + ) + + if dist <= 1 or (self.bot.config.walk > 0 and is_at_destination): + if (self.ptr + 1) == len(self.points): + self.ptr = 0 + if self.path_mode == 'linear': + self.points = list(reversed(self.points)) + else: + self.ptr += 1 + + return [lat, lng] diff --git a/pokemongo_bot/cell_workers/follow_spiral.py b/pokemongo_bot/cell_workers/follow_spiral.py new file mode 100644 index 0000000000..28b548d1ca --- /dev/null +++ b/pokemongo_bot/cell_workers/follow_spiral.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import math + +from pokemongo_bot.cell_workers.utils import distance, format_dist +from pokemongo_bot.step_walker import StepWalker +from pokemongo_bot.cell_workers.base_task import BaseTask + +class FollowSpiral(BaseTask): + def initialize(self): + self.steplimit = self.config.get("diameter", 4) + self.step_size = self.config.get("step_size", 70) + self.origin_lat = self.bot.position[0] + self.origin_lon = self.bot.position[1] + + self.diameter_to_steps = (self.steplimit+1) ** 2 + self.points = self._generate_spiral( + self.origin_lat, self.origin_lon, self.step_size, self.diameter_to_steps + ) + + self.ptr = 0 + self.direction = 1 + self.cnt = 0 + + + @staticmethod + def _generate_spiral(starting_lat, starting_lng, step_size, step_limit): + """ + Sourced from: + https://github.com/tejado/pgoapi/blob/master/examples/spiral_poi_search.py + + :param starting_lat: + :param starting_lng: + :param step_size: + :param step_limit: + :return: + """ + coords = [{'lat': starting_lat, 'lng': starting_lng}] + steps, x, y, d, m = 1, 0, 0, 1, 1 + + rlat = starting_lat * math.pi + latdeg = 111132.93 - 559.82 * math.cos(2*rlat) + 1.175*math.cos(4*rlat) + lngdeg = 111412.84 * math.cos(rlat) - 93.5 * math.cos(3*rlat) + step_size_lat = step_size / latdeg + step_size_lng = step_size / lngdeg + + while steps < step_limit: + while 2 * x * d < m and steps < step_limit: + x = x + d + steps += 1 + lat = x * step_size_lat + starting_lat + lng = y * step_size_lng + starting_lng + coords.append({'lat': lat, 'lng': lng}) + while 2 * y * d < m and steps < step_limit: + y = y + d + steps += 1 + lat = x * step_size_lat + starting_lat + lng = y * step_size_lng + starting_lng + coords.append({'lat': lat, 'lng': lng}) + + d *= -1 + m += 1 + return coords + + def work(self): + point = self.points[self.ptr] + self.cnt += 1 + + if self.bot.config.walk > 0: + step_walker = StepWalker( + self.bot, + self.bot.config.walk, + point['lat'], + point['lng'] + ) + + dist = distance( + self.bot.api._position_lat, + self.bot.api._position_lng, + point['lat'], + point['lng'] + ) + + if self.cnt == 1: + self.emit_event( + 'position_update', + formatted="Walking from {last_position} to {current_position} ({distance} {distance_unit})", + data={ + 'last_position': self.bot.position, + 'current_position': (point['lat'], point['lng'], 0), + 'distance': dist, + 'distance_unit': 'm' + } + ) + + if step_walker.step(): + step_walker = None + else: + self.bot.api.set_position(point['lat'], point['lng']) + + if distance( + self.bot.api._position_lat, + self.bot.api._position_lng, + point['lat'], + point['lng'] + ) <= 1 or (self.bot.config.walk > 0 and step_walker == None): + if self.ptr + self.direction >= len(self.points) or self.ptr + self.direction <= -1: + self.direction *= -1 + if len(self.points) != 1: + self.ptr += self.direction + else: + self.ptr = 0 + self.cnt = 0 + + return [point['lat'], point['lng']] diff --git a/pokemongo_bot/cell_workers/handle_soft_ban.py b/pokemongo_bot/cell_workers/handle_soft_ban.py new file mode 100644 index 0000000000..e266c5c377 --- /dev/null +++ b/pokemongo_bot/cell_workers/handle_soft_ban.py @@ -0,0 +1,69 @@ +from random import randint + +from pgoapi.utilities import f2i + +from pokemongo_bot.constants import Constants +from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.cell_workers import MoveToFort +from pokemongo_bot.cell_workers.utils import distance +from pokemongo_bot.worker_result import WorkerResult + + +class HandleSoftBan(BaseTask): + def work(self): + if not self.should_run(): + return + + forts = self.bot.get_forts(order_by_distance=True) + + if len(forts) == 0: + return + + fort_distance = distance( + self.bot.position[0], + self.bot.position[1], + forts[0]['latitude'], + forts[0]['longitude'], + ) + + if fort_distance > Constants.MAX_DISTANCE_FORT_IS_REACHABLE: + MoveToFort(self.bot, config=None).work() + self.bot.recent_forts = self.bot.recent_forts[0:-1] + if forts[0]['id'] in self.bot.fort_timeouts: + del self.bot.fort_timeouts[forts[0]['id']] + return WorkerResult.RUNNING + else: + spins = randint(50,60) + self.emit_event( + 'softban_fix', + formatted='Fixing softban.' + ) + for i in xrange(spins): + self.spin_fort(forts[0]) + self.bot.softban = False + self.emit_event( + 'softban_fix_done', + formatted='Softban should be fixed' + ) + + def spin_fort(self, fort): + self.bot.api.fort_search( + fort_id=fort['id'], + fort_latitude=fort['latitude'], + fort_longitude=fort['longitude'], + player_latitude=f2i(self.bot.position[0]), + player_longitude=f2i(self.bot.position[1]) + ) + self.bot.event_handler.emit( + 'spun_fort', + level='debug', + formatted="Spun fort {fort_id}", + data={ + 'fort_id': fort_id, + 'lat': fort['latitude'], + 'lng': fort['longitude'] + } + ) + + def should_run(self): + return self.bot.softban diff --git a/pokemongo_bot/cell_workers/incubate_eggs.py b/pokemongo_bot/cell_workers/incubate_eggs.py new file mode 100644 index 0000000000..9e21b0d280 --- /dev/null +++ b/pokemongo_bot/cell_workers/incubate_eggs.py @@ -0,0 +1,207 @@ +from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.cell_workers.base_task import BaseTask + + +class IncubateEggs(BaseTask): + last_km_walked = 0 + + def initialize(self): + self.ready_incubators = [] + self.used_incubators = [] + self.eggs = [] + self.km_walked = 0 + self.hatching_animation_delay = 4.20 + self.max_iv = 45.0 + + self._process_config() + + def _process_config(self): + self.longer_eggs_first = self.config.get("longer_eggs_first", True) + + def work(self): + try: + self._check_inventory() + except: + return + + if self.used_incubators and IncubateEggs.last_km_walked != self.km_walked: + self.used_incubators.sort(key=lambda x: x.get("km")) + km_left = self.used_incubators[0]['km']-self.km_walked + if km_left <= 0: + self._hatch_eggs() + else: + self.emit_event( + 'next_egg_incubates', + formatted='Next egg incubates in {distance_in_km:.2f} km', + data={ + 'distance_in_km': km_left + } + ) + IncubateEggs.last_km_walked = self.km_walked + + sorting = self.longer_eggs_first + self.eggs.sort(key=lambda x: x.get("km"), reverse=sorting) + + if self.ready_incubators: + self._apply_incubators() + + def _apply_incubators(self): + for incubator in self.ready_incubators: + for egg in self.eggs: + if egg["used"] or egg["km"] == -1: + continue + self.emit_event( + 'incubate_try', + level='debug', + formatted="Attempting to apply incubator {incubator_id} to egg {egg_id}", + data={ + 'incubator_id': incubator['id'], + 'egg_id': egg['id'] + } + ) + ret = self.bot.api.use_item_egg_incubator( + item_id=incubator["id"], + pokemon_id=egg["id"] + ) + if ret: + code = ret.get("responses", {}).get("USE_ITEM_EGG_INCUBATOR", {}).get("result", 0) + if code == 1: + self.emit_event( + 'incubate', + formatted='Incubating a {distance_in_km} egg.', + data={ + 'distance_in_km': str(egg['km']) + } + ) + egg["used"] = True + incubator["used"] = True + break + elif code == 5 or code == 7: + self.emit_event( + 'incubator_already_used', + level='debug', + formatted='Incubator in use.', + ) + incubator["used"] = True + break + elif code == 6: + self.emit_event( + 'egg_already_incubating', + level='debug', + formatted='Egg already incubating', + ) + egg["used"] = True + + def _check_inventory(self, lookup_ids=[]): + inv = {} + response_dict = self.bot.get_inventory() + matched_pokemon = [] + temp_eggs = [] + temp_used_incubators = [] + temp_ready_incubators = [] + inv = reduce( + dict.__getitem__, + ["responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], + response_dict + ) + for inv_data in inv: + inv_data = inv_data.get("inventory_item_data", {}) + if "egg_incubators" in inv_data: + temp_used_incubators = [] + temp_ready_incubators = [] + incubators = inv_data.get("egg_incubators", {}).get("egg_incubator",[]) + if isinstance(incubators, basestring): # checking for old response + incubators = [incubators] + for incubator in incubators: + if 'pokemon_id' in incubator: + temp_used_incubators.append({ + "id": incubator.get('id', -1), + "km": incubator.get('target_km_walked', 9001) + }) + else: + temp_ready_incubators.append({ + "id": incubator.get('id', -1) + }) + continue + if "pokemon_data" in inv_data: + pokemon = inv_data.get("pokemon_data", {}) + if pokemon.get("is_egg", False) and "egg_incubator_id" not in pokemon: + temp_eggs.append({ + "id": pokemon.get("id", -1), + "km": pokemon.get("egg_km_walked_target", -1), + "used": False + }) + elif 'is_egg' not in pokemon and pokemon['id'] in lookup_ids: + pokemon.update({ + "iv": [ + pokemon.get('individual_attack', 0), + pokemon.get('individual_defense', 0), + pokemon.get('individual_stamina', 0) + ]}) + matched_pokemon.append(pokemon) + continue + if "player_stats" in inv_data: + self.km_walked = inv_data.get("player_stats", {}).get("km_walked", 0) + if temp_used_incubators: + self.used_incubators = temp_used_incubators + if temp_ready_incubators: + self.ready_incubators = temp_ready_incubators + if temp_eggs: + self.eggs = temp_eggs + return matched_pokemon + + def _hatch_eggs(self): + response_dict = self.bot.api.get_hatched_eggs() + log_color = 'green' + try: + result = reduce(dict.__getitem__, ["responses", "GET_HATCHED_EGGS"], response_dict) + except KeyError: + return + pokemon_ids = [] + if 'pokemon_id' in result: + pokemon_ids = [id for id in result['pokemon_id']] + stardust = result.get('stardust_awarded', "error") + candy = result.get('candy_awarded', "error") + xp = result.get('experience_awarded', "error") + sleep(self.hatching_animation_delay) + self.bot.latest_inventory = None + try: + pokemon_data = self._check_inventory(pokemon_ids) + for pokemon in pokemon_data: + # pokemon ids seem to be offset by one + if pokemon['pokemon_id']!=-1: + pokemon['name'] = self.bot.pokemon_list[(pokemon.get('pokemon_id')-1)]['Name'] + else: + pokemon['name'] = "error" + except: + pokemon_data = [{"name":"error","cp":"error","iv":"error"}] + if not pokemon_ids or pokemon_data[0]['name'] == "error": + self.emit_event( + 'egg_hatched', + data={ + 'pokemon': 'error', + 'cp': 'error', + 'iv': 'error', + 'exp': 'error', + 'stardust': 'error', + 'candy': 'error', + } + ) + return + for i in range(len(pokemon_data)): + msg = "Egg hatched with a {pokemon} (CP {cp} - IV {iv}), {exp} exp, {stardust} stardust and {candy} candies." + self.emit_event( + 'egg_hatched', + formatted=msg, + data={ + 'pokemon': pokemon_data[i]['name'], + 'cp': pokemon_data[i]['cp'], + 'iv': "{} {}".format( + "/".join(map(str, pokemon_data[i]['iv'])), + sum(pokemon_data[i]['iv'])/self.max_iv + ), + 'exp': xp[i], + 'stardust': stardust[i], + 'candy': candy[i], + } + ) diff --git a/pokemongo_bot/cell_workers/initial_transfer_worker.py b/pokemongo_bot/cell_workers/initial_transfer_worker.py deleted file mode 100644 index 026e79ddaa..0000000000 --- a/pokemongo_bot/cell_workers/initial_transfer_worker.py +++ /dev/null @@ -1,75 +0,0 @@ -import json - -from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot import logger - -class InitialTransferWorker(object): - def __init__(self, bot): - self.config = bot.config - self.pokemon_list = bot.pokemon_list - self.api = bot.api - - def work(self): - logger.log('[x] Initial Transfer.') - - logger.log( - '[x] Preparing to transfer all duplicate Pokemon, keeping the highest CP of each type.') - - logger.log('[x] Will NOT transfer anything above CP {}'.format( - self.config.initial_transfer)) - - pokemon_groups = self._initial_transfer_get_groups() - - for id in pokemon_groups: - - group_cp = pokemon_groups[id].keys() - - if len(group_cp) > 1: - group_cp.sort() - group_cp.reverse() - - - for x in range(1, len(group_cp)): - if self.config.initial_transfer and group_cp[x] > self.config.initial_transfer: - continue - - print('[x] Transferring {} with CP {}'.format( - self.pokemon_list[id - 1]['Name'], group_cp[x])) - self.api.release_pokemon( - pokemon_id=pokemon_groups[id][group_cp[x]]) - response_dict = self.api.call() - sleep(2) - - logger.log('[x] Transferring Done.') - - def _initial_transfer_get_groups(self): - pokemon_groups = {} - self.api.get_player().get_inventory() - inventory_req = self.api.call() - inventory_dict = inventory_req['responses']['GET_INVENTORY'][ - 'inventory_delta']['inventory_items'] - - user_web_inventory = 'web/inventory-%s.json' % (self.config.username) - with open(user_web_inventory, 'w') as outfile: - json.dump(inventory_dict, outfile) - - for pokemon in inventory_dict: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data", "pokemon_id" - ], pokemon) - except KeyError: - continue - - group_id = pokemon['inventory_item_data'][ - 'pokemon_data']['pokemon_id'] - group_pokemon = pokemon['inventory_item_data'][ - 'pokemon_data']['id'] - group_pokemon_cp = pokemon[ - 'inventory_item_data']['pokemon_data']['cp'] - - if group_id not in pokemon_groups: - pokemon_groups[group_id] = {} - - pokemon_groups[group_id].update({group_pokemon_cp: group_pokemon}) - return pokemon_groups diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py new file mode 100644 index 0000000000..f43d1641e6 --- /dev/null +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from pokemongo_bot.constants import Constants +from pokemongo_bot.step_walker import StepWalker +from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.cell_workers.base_task import BaseTask +from utils import distance, format_dist, fort_details + + +class MoveToFort(BaseTask): + + def initialize(self): + self.lure_distance = 0 + self.lure_attraction = True #self.config.get("lure_attraction", True) + self.lure_max_distance = 2000 #self.config.get("lure_max_distance", 2000) + + def should_run(self): + has_space_for_loot = self.bot.has_space_for_loot() + if not has_space_for_loot: + self.emit_event( + 'inventory_full', + formatted="Not moving to any forts as there aren't enough space. You might want to change your config to recycle more items if this message appears consistently." + ) + return has_space_for_loot or self.bot.softban + + def is_attracted(self): + return (self.lure_distance > 0) + + def work(self): + if not self.should_run(): + return WorkerResult.SUCCESS + + nearest_fort = self.get_nearest_fort() + + if nearest_fort is None: + return WorkerResult.SUCCESS + + lat = nearest_fort['latitude'] + lng = nearest_fort['longitude'] + fortID = nearest_fort['id'] + details = fort_details(self.bot, fortID, lat, lng) + fort_name = details.get('name', 'Unknown') + + unit = self.bot.config.distance_unit # Unit to use when printing formatted distance + + dist = distance( + self.bot.position[0], + self.bot.position[1], + lat, + lng + ) + + if dist > Constants.MAX_DISTANCE_FORT_IS_REACHABLE: + fort_event_data = { + 'fort_name': u"{}".format(fort_name), + 'distance': format_dist(dist, unit), + } + + if self.is_attracted() > 0: + fort_event_data.update(lure_distance=format_dist(self.lure_distance, unit)) + self.emit_event( + 'moving_to_lured_fort', + formatted="Moving towards pokestop {fort_name} - {distance} (attraction of lure {lure_distance})", + data=fort_event_data + ) + else: + self.emit_event( + 'moving_to_fort', + formatted="Moving towards pokestop {fort_name} - {distance}", + data=fort_event_data + ) + + step_walker = StepWalker( + self.bot, + self.bot.config.walk, + lat, + lng + ) + + if not step_walker.step(): + return WorkerResult.RUNNING + + self.emit_event( + 'arrived_at_fort', + formatted='Arrived at fort.' + ) + return WorkerResult.SUCCESS + + def _get_nearest_fort_on_lure_way(self, forts): + + if not self.lure_attraction: + return None, 0 + + lures = filter(lambda x: True if x.get('lure_info', None) != None else False, forts) + + if (len(lures)): + dist_lure_me = distance(self.bot.position[0], self.bot.position[1], + lures[0]['latitude'],lures[0]['longitude']) + else: + dist_lure_me = 0 + + if dist_lure_me > 0 and dist_lure_me < self.lure_max_distance: + + self.lure_distance = dist_lure_me + + for fort in forts: + dist_lure_fort = distance( + fort['latitude'], + fort['longitude'], + lures[0]['latitude'], + lures[0]['longitude']) + dist_fort_me = distance( + fort['latitude'], + fort['longitude'], + self.bot.position[0], + self.bot.position[1]) + + if dist_lure_fort < dist_lure_me and dist_lure_me > dist_fort_me: + return fort, dist_lure_me + + if dist_fort_me > dist_lure_me: + break + + return lures[0], dist_lure_me + + else: + return None, 0 + + def get_nearest_fort(self): + forts = self.bot.get_forts(order_by_distance=True) + + # Remove stops that are still on timeout + forts = filter(lambda x: x["id"] not in self.bot.fort_timeouts, forts) + + next_attracted_pts, lure_distance = self._get_nearest_fort_on_lure_way(forts) + + # Remove all forts which were spun in the last ticks to avoid circles if set + if self.bot.config.forts_avoid_circles: + forts = filter(lambda x: x["id"] not in self.bot.recent_forts, forts) + + self.lure_distance = lure_distance + + if (lure_distance > 0): + return next_attracted_pts + + if len(forts) > 0: + return forts[0] + else: + return None diff --git a/pokemongo_bot/cell_workers/move_to_fort_worker.py b/pokemongo_bot/cell_workers/move_to_fort_worker.py deleted file mode 100644 index 3af7befcd3..0000000000 --- a/pokemongo_bot/cell_workers/move_to_fort_worker.py +++ /dev/null @@ -1,40 +0,0 @@ -from utils import distance, format_dist -from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot import logger - -class MoveToFortWorker(object): - def __init__(self, fort, bot): - self.fort = fort - self.api = bot.api - self.config = bot.config - self.stepper = bot.stepper - self.position = bot.position - - def work(self): - lat = self.fort['latitude'] - lng = self.fort['longitude'] - fortID = self.fort['id'] - unit = self.config.distance_unit # Unit to use when printing formatted distance - - dist = distance(self.position[0], self.position[1], lat, lng) - - # print('[#] Found fort {} at distance {}m'.format(fortID, dist)) - logger.log('[#] Found fort {} at distance {}'.format( - fortID, format_dist(dist, unit))) - - if dist > 10: - logger.log('[#] Need to move closer to Pokestop') - position = (lat, lng, 0.0) - - if self.config.walk > 0: - self.stepper._walk_to(self.config.walk, *position) - else: - self.api.set_position(*position) - - self.api.player_update(latitude=lat, longitude=lng) - response_dict = self.api.call() - logger.log('[#] Arrived at Pokestop') - sleep(2) - return response_dict - - return None diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py new file mode 100644 index 0000000000..bce39e0143 --- /dev/null +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- + +import os +import time +import json +import base64 +import requests +from pokemongo_bot import logger +from pokemongo_bot.cell_workers.utils import distance, format_dist, format_time +from pokemongo_bot.step_walker import StepWalker +from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker + + +class MoveToMapPokemon(BaseTask): + def initialize(self): + self.last_map_update = 0 + self.pokemon_data = self.bot.pokemon_list + self.unit = self.bot.config.distance_unit + self.caught = [] + + data_file = 'data/map-caught-{}.json'.format(self.bot.config.username) + if os.path.isfile(data_file): + self.caught = json.load( + open(data_file) + ) + + def get_pokemon_from_map(self): + try: + req = requests.get('{}/raw_data?gyms=false&scanned=false'.format(self.config['address'])) + except requests.exceptions.ConnectionError: + logger.log('Could not reach PokemonGo-Map Server', 'red') + return [] + + try: + raw_data = req.json() + except ValueError: + logger.log('Map data was not valid', 'red') + return [] + + pokemon_list = [] + now = int(time.time()) + + for pokemon in raw_data['pokemons']: + try: + pokemon['encounter_id'] = long(base64.b64decode(pokemon['encounter_id'])) + except TypeError: + log.logger('base64 error: {}'.format(pokemon['encounter_id']), 'red') + continue + pokemon['spawn_point_id'] = pokemon['spawnpoint_id'] + pokemon['disappear_time'] = int(pokemon['disappear_time'] / 1000) + pokemon['name'] = self.pokemon_data[pokemon['pokemon_id'] - 1]['Name'] + pokemon['is_vip'] = pokemon['name'] in self.bot.config.vips + + if pokemon['name'] not in self.config['catch'] and not pokemon['is_vip']: + continue + + if pokemon['disappear_time'] < (now + self.config['min_time']): + continue + + if self.was_caught(pokemon): + continue + + pokemon['priority'] = self.config['catch'].get(pokemon['name'], 0) + + pokemon['dist'] = distance( + self.bot.position[0], + self.bot.position[1], + pokemon['latitude'], + pokemon['longitude'], + ) + + if pokemon['dist'] > self.config['max_distance'] and not self.config['snipe']: + continue + + pokemon_list.append(pokemon) + + return pokemon_list + + def add_caught(self, pokemon): + for caught_pokemon in self.caught: + if caught_pokemon['encounter_id'] == pokemon['encounter_id']: + return + if len(self.caught) >= 200: + self.caught.pop(0) + self.caught.append(pokemon) + + def was_caught(self, pokemon): + for caught_pokemon in self.caught: + if pokemon['encounter_id'] == caught_pokemon['encounter_id']: + return True + return False + + def update_map_location(self): + if not self.config['update_map']: + return + try: + req = requests.get('{}/loc'.format(self.config['address'])) + except requests.exceptions.ConnectionError: + logger.log('Could not reach PokemonGo-Map Server', 'red') + return + + try: + loc_json = req.json() + except ValueError: + return log.logger('Map location data was not valid', 'red') + + + dist = distance( + self.bot.position[0], + self.bot.position[1], + loc_json['lat'], + loc_json['lng'] + ) + + # update map when 500m away from center and last update longer than 2 minutes away + now = int(time.time()) + if dist > 500 and now - self.last_map_update > 2 * 60: + requests.post('{}/next_loc?lat={}&lon={}'.format(self.config['address'], self.bot.position[0], self.bot.position[1])) + logger.log('Updated PokemonGo-Map position') + self.last_map_update = now + + def snipe(self, pokemon): + last_position = self.bot.position[0:2] + + self.bot.heartbeat() + + logger.log('Teleporting to {} ({})'.format(pokemon['name'], format_dist(pokemon['dist'], self.unit)), 'green') + self.bot.api.set_position(pokemon['latitude'], pokemon['longitude'], 0) + + logger.log('Encounter pokemon', 'green') + catch_worker = PokemonCatchWorker(pokemon, self.bot) + api_encounter_response = catch_worker.create_encounter_api_call() + + time.sleep(2) + logger.log('Teleporting back to previous location..', 'green') + self.bot.api.set_position(last_position[0], last_position[1], 0) + time.sleep(2) + self.bot.heartbeat() + + catch_worker.work(api_encounter_response) + self.add_caught(pokemon) + + return WorkerResult.SUCCESS + + def dump_caught_pokemon(self): + user_data_map_caught = 'data/map-caught-{}.json'.format(self.bot.config.username) + with open(user_data_map_caught, 'w') as outfile: + json.dump(self.caught, outfile) + + def work(self): + # check for pokeballs (excluding masterball) + pokeballs = self.bot.item_inventory_count(1) + superballs = self.bot.item_inventory_count(2) + ultraballs = self.bot.item_inventory_count(3) + + if (pokeballs + superballs + ultraballs) < 1: + return WorkerResult.SUCCESS + + self.update_map_location() + self.dump_caught_pokemon() + + pokemon_list = self.get_pokemon_from_map() + pokemon_list.sort(key=lambda x: x['dist']) + if self.config['mode'] == 'priority': + pokemon_list.sort(key=lambda x: x['priority'], reverse=True) + if self.config['prioritize_vips']: + pokemon_list.sort(key=lambda x: x['is_vip'], reverse=True) + + if len(pokemon_list) < 1: + return WorkerResult.SUCCESS + + pokemon = pokemon_list[0] + + # if we only have ultraballs and the target is not a vip don't snipe/walk + if (pokeballs + superballs) < 1 and not pokemon['is_vip']: + return WorkerResult.SUCCESS + + if self.config['snipe']: + return self.snipe(pokemon) + + now = int(time.time()) + logger.log('Moving towards {}, {} left ({})'.format(pokemon['name'], format_dist(pokemon['dist'], self.unit), format_time(pokemon['disappear_time'] - now))) + step_walker = StepWalker( + self.bot, + self.bot.config.walk, + pokemon['latitude'], + pokemon['longitude'] + ) + + if not step_walker.step(): + return WorkerResult.RUNNING + + logger.log('Arrived at {}'.format(pokemon['name'])) + self.add_caught(pokemon) + return WorkerResult.SUCCESS diff --git a/pokemongo_bot/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py new file mode 100644 index 0000000000..29df15ae4a --- /dev/null +++ b/pokemongo_bot/cell_workers/nickname_pokemon.py @@ -0,0 +1,101 @@ +from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.cell_workers.base_task import BaseTask + +class NicknamePokemon(BaseTask): + def initialize(self): + self.template = self.config.get('nickname_template','').lower().strip() + if self.template == "{name}": + self.template = "" + + def work(self): + try: + inventory = reduce(dict.__getitem__, ["responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], self.bot.get_inventory()) + except KeyError: + pass + else: + pokemon_data = self._get_inventory_pokemon(inventory) + for pokemon in pokemon_data: + self._nickname_pokemon(pokemon) + + def _get_inventory_pokemon(self,inventory_dict): + pokemon_data = [] + for inv_data in inventory_dict: + try: + pokemon = reduce(dict.__getitem__,['inventory_item_data','pokemon_data'],inv_data) + except KeyError: + pass + else: + if not pokemon.get('is_egg',False): + pokemon_data.append(pokemon) + return pokemon_data + + def _nickname_pokemon(self,pokemon): + """This requies a pokemon object containing all the standard fields: id, ivs, cp, etc""" + new_name = "" + instance_id = pokemon.get('id',0) + if not instance_id: + self.emit_event( + 'api_error', + formatted='Failed to get pokemon name, will not rename.' + ) + return + id = pokemon.get('pokemon_id',0)-1 + name = self.bot.pokemon_list[id]['Name'] + cp = pokemon.get('cp',0) + iv_attack = pokemon.get('individual_attack',0) + iv_defense = pokemon.get('individual_defense',0) + iv_stamina = pokemon.get('individual_stamina',0) + iv_list = [iv_attack,iv_defense,iv_stamina] + iv_ads = "/".join(map(str,iv_list)) + iv_sum = sum(iv_list) + iv_pct = "{:0.0f}".format(100*iv_sum/45.0) + log_color = 'red' + try: + new_name = self.template.format(name=name, + id=id, + cp=cp, + iv_attack=iv_attack, + iv_defense=iv_defense, + iv_stamina=iv_stamina, + iv_ads=iv_ads, + iv_sum=iv_sum, + iv_pct=iv_pct)[:12] + except KeyError as bad_key: + self.emit_event( + 'config_error', + formatted="Unable to nickname {} due to bad template ({})".format(name,bad_key) + ) + if pokemon.get('nickname', '') == new_name: + return + response = self.bot.api.nickname_pokemon(pokemon_id=instance_id,nickname=new_name) + sleep(1.2) + try: + result = reduce(dict.__getitem__, ["responses", "NICKNAME_POKEMON"], response) + except KeyError: + self.emit_event( + 'api_error', + formatted='Attempt to nickname received bad response from server.' + ) + result = result['result'] + new_name = new_name or name + if result == 0: + self.emit_event( + 'unset_pokemon_nickname', + formatted="Pokemon nickname unset." + ) + elif result == 1: + self.emit_event( + 'rename_pokemon', + formatted="Pokemon {old_name} renamed to {current_name}", + data={ + 'old_name': name, + 'current_name': new_name + } + ) + pokemon['nickname'] = new_name + elif result == 2: + self.emit_event( + 'pokemon_nickname_invalid', + formatted="Nickname {nickname} is invalid", + data={'nickname': new_name} + ) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 05ace03865..afa5578267 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- import time -from sets import Set -from utils import distance -from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot import logger +from pokemongo_bot.human_behaviour import (normalized_reticle_size, sleep, + spin_modifier) +from pokemongo_bot.cell_workers.base_task import BaseTask -class PokemonCatchWorker(object): +class PokemonCatchWorker(BaseTask): BAG_FULL = 'bag_full' NO_POKEBALLS = 'no_pokeballs' @@ -19,288 +18,502 @@ def __init__(self, pokemon, bot): self.pokemon_list = bot.pokemon_list self.item_list = bot.item_list self.inventory = bot.inventory + self.spawn_point_guid = '' + self.response_key = '' + self.response_status_key = '' - def work(self): + def work(self, response_dict=None): encounter_id = self.pokemon['encounter_id'] - spawnpoint_id = self.pokemon['spawnpoint_id'] - player_latitude = self.pokemon['latitude'] - player_longitude = self.pokemon['longitude'] - self.api.encounter(encounter_id=encounter_id, spawnpoint_id=spawnpoint_id, - player_latitude=player_latitude, player_longitude=player_longitude) - response_dict = self.api.call() - if response_dict and 'responses' in response_dict: - if 'ENCOUNTER' in response_dict['responses']: - if 'status' in response_dict['responses']['ENCOUNTER']: - if response_dict['responses']['ENCOUNTER']['status'] is 7: - logger.log('[x] Pokemon Bag is full!', 'red') - return PokemonCatchWorker.BAG_FULL + if not response_dict: + response_dict = self.create_encounter_api_call() - if response_dict['responses']['ENCOUNTER']['status'] is 1: + if response_dict and 'responses' in response_dict: + if self.response_key in response_dict['responses']: + if self.response_status_key in response_dict['responses'][self.response_key]: + if response_dict['responses'][self.response_key][self.response_status_key] is 1: cp = 0 - total_IV = 0 - if 'wild_pokemon' in response_dict['responses']['ENCOUNTER']: - pokemon = response_dict['responses']['ENCOUNTER']['wild_pokemon'] - catch_rate = response_dict['responses']['ENCOUNTER']['capture_probability']['capture_probability'] # 0 = pokeballs, 1 great balls, 3 ultra balls + if 'wild_pokemon' in response_dict['responses'][self.response_key] or 'pokemon_data' in \ + response_dict['responses'][self.response_key]: + if self.response_key == 'ENCOUNTER': + pokemon = response_dict['responses'][self.response_key]['wild_pokemon'] + else: + pokemon = response_dict['responses'][self.response_key] + + catch_rate = response_dict['responses'][self.response_key]['capture_probability'][ + 'capture_probability'] # 0 = pokeballs, 1 great balls, 3 ultra balls if 'pokemon_data' in pokemon and 'cp' in pokemon['pokemon_data']: - cp = pokemon['pokemon_data']['cp'] - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - - for individual_stat in iv_stats: - try: - total_IV += pokemon['pokemon_data'][individual_stat] - except: - pokemon['pokemon_data'][individual_stat] = 0 - continue - - pokemon_potential = round((total_IV / 45.0), 2) - pokemon_num = int(pokemon['pokemon_data'][ - 'pokemon_id']) - 1 - pokemon_name = self.pokemon_list[ - int(pokemon_num)]['Name'] - logger.log('[#] A Wild {} appeared! [CP {}] [Potential {}]'.format( - pokemon_name, cp, pokemon_potential), 'yellow') - - logger.log('[#] IV [Stamina/Attack/Defense] = [{}/{}/{}]'.format( - pokemon['pokemon_data']['individual_stamina'], - pokemon['pokemon_data']['individual_attack'], - pokemon['pokemon_data']['individual_defense'] - )) - pokemon['pokemon_data']['name'] = pokemon_name + pokemon_data = pokemon['pokemon_data'] + cp = pokemon_data['cp'] + + individual_attack = pokemon_data.get("individual_attack", 0) + individual_stamina = pokemon_data.get("individual_stamina", 0) + individual_defense = pokemon_data.get("individual_defense", 0) + + iv_display = '{}/{}/{}'.format( + individual_attack, + individual_defense, + individual_stamina + ) + + pokemon_potential = self.pokemon_potential(pokemon_data) + pokemon_num = int(pokemon_data['pokemon_id']) - 1 + pokemon_name = self.pokemon_list[int(pokemon_num)]['Name'] + + msg = 'A wild {pokemon} appeared! [CP {cp}] [Potential {iv}] [S/A/D {iv_display}]' + self.emit_event( + 'pokemon_appeared', + formatted=msg, + data={ + 'pokemon': pokemon_name, + 'cp': cp, + 'iv': pokemon_potential, + 'iv_display': iv_display, + } + ) + + pokemon_data['name'] = pokemon_name # Simulate app sleep(3) - balls_stock = self.bot.pokeball_inventory() - while(True): - - pokeball = 1 # default:poke ball - - if balls_stock[1] <= 0: # if poke ball are out of stock - if balls_stock[2] > 0: # and player has great balls in stock... - pokeball = 2 # then use great balls - elif balls_stock[3] > 0: # or if great balls are out of stock too, and player has ultra balls... - pokeball = 3 # then use ultra balls - else: - pokeball = 0 # player doesn't have any of pokeballs, great balls or ultra balls + if not self.should_capture_pokemon(pokemon_name, cp, pokemon_potential, response_dict): + return False + + flag_VIP = False + # @TODO, use the best ball in stock to catch VIP (Very Important Pokemon: Configurable) + if self.check_vip_pokemon(pokemon_name, cp, pokemon_potential): + self.emit_event( + 'vip_pokemon', + formatted='This is a VIP pokemon. Catch!!!' + ) + flag_VIP=True + + items_stock = self.bot.current_inventory() + berry_id = 701 # @ TODO: use better berries if possible + berries_count = self.bot.item_inventory_count(berry_id) + while True: + # pick the most simple ball from stock + pokeball = 1 # start from 1 - PokeBalls + berry_used = False + + if flag_VIP: + if(berries_count>0 and catch_rate[pokeball-1] < 0.9): + success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) + self.emit_event( + 'pokemon_catch_rate', + level='debug', + formatted="Catch rate of {catch_rate} is low. Maybe will throw {berry_name} ({berry_count} left)", + data={ + 'catch_rate': success_percentage, + 'berry_name': self.item_list[str(berry_id)], + 'berry_count': berries_count + } + ) + # Out of all pokeballs! Let's don't waste berry. + if items_stock[1] == 0 and items_stock[2] == 0 and items_stock[3] == 0: + break + + # Use the berry to catch + response_dict = self.api.use_item_capture( + item_id=berry_id, + encounter_id=encounter_id, + spawn_point_id=self.spawn_point_guid + ) + if response_dict and response_dict['status_code'] is 1 and 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: + for i in range(len(catch_rate)): + if 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: + catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] + success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) + berries_count = berries_count -1 + berry_used = True + self.emit_event( + 'threw_berry', + formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", + data={ + "berry_name": self.item_list[str(berry_id)], + "new_catch_rate": success_percentage + } + ) + else: + if response_dict['status_code'] is 1: + self.emit_event( + 'softban', + level='warning', + formatted='Failed to use berry. You may be softbanned.' + ) + else: + self.emit_event( + 'threw_berry_failed', + formatted='Unknown response when throwing berry: {status_code}.', + data={ + 'status_code': response_dict['status_code'] + } + ) + + #use the best ball to catch + current_type = pokeball + #debug use normal ball + while current_type < 3: + current_type += 1 + if catch_rate[pokeball-1] < 0.9 and items_stock[current_type] > 0: + # if current ball chance to catch is under 90%, and player has better ball - then use it + pokeball = current_type # use better ball + else: + # If we have a lot of berries (than the great ball), we prefer use a berry first! + if catch_rate[pokeball-1] < 0.42 and items_stock[pokeball+1]+30 < berries_count: + # If it's not the VIP type, we don't want to waste our ultra ball if no balls left. + if items_stock[1] == 0 and items_stock[2] == 0: + break + + success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) + self.emit_event( + 'pokemon_catch_rate', + level='debug', + formatted="Catch rate of {catch_rate} is low. Maybe will throw {berry_name} ({berry_count} left)", + data={ + 'catch_rate': success_percentage, + 'berry_name': self.item_list[str(berry_id)], + 'berry_count': berries_count-1 + } + ) + response_dict = self.api.use_item_capture(item_id=berry_id, + encounter_id=encounter_id, + spawn_point_id=self.spawn_point_guid + ) + if response_dict and response_dict['status_code'] is 1 and 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: + for i in range(len(catch_rate)): + if 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: + catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] + success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) + berries_count = berries_count -1 + berry_used = True + self.emit_event( + 'threw_berry', + formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", + data={ + "berry_name": self.item_list[str(berry_id)], + "new_catch_rate": success_percentage + } + ) + else: + if response_dict['status_code'] is 1: + self.emit_event( + 'softban', + level='warning', + formatted='Failed to use berry. You may be softbanned.' + ) + else: + self.emit_event( + 'threw_berry_failed', + formatted='Unknown response when throwing berry: {status_code}.', + data={ + 'status_code': response_dict['status_code'] + } + ) - while(pokeball < 3): - if catch_rate[pokeball-1] < 0.35 and balls_stock[pokeball+1] > 0: - # if current ball chance to catch is under 35%, and player has better ball - then use it - pokeball = pokeball+1 # use better ball else: - break - - # @TODO, use the best ball in stock to catch VIP (Very Important Pokemon: Configurable) + #We don't have many berry to waste, pick a good ball first. Save some berry for future VIP pokemon + current_type = pokeball + while current_type < 2: + current_type += 1 + if catch_rate[pokeball-1] < 0.35 and items_stock[current_type] > 0: + # if current ball chance to catch is under 35%, and player has better ball - then use it + pokeball = current_type # use better ball + + #if the rate is still low and we didn't throw a berry before use berry + if catch_rate[pokeball-1] < 0.35 and berries_count > 0 and berry_used == False: + # If it's not the VIP type, we don't want to waste our ultra ball if no balls left. + if items_stock[1] == 0 and items_stock[2] == 0: + break + + success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) + self.emit_event( + 'pokemon_catch_rate', + level='debug', + formatted="Catch rate of {catch_rate} is low. Throwing {berry_name} ({berry_count} left)", + data={ + 'catch_rate': success_percentage, + 'berry_name': self.item_list[str(berry_id)], + 'berry_count': berries_count-1 + } + ) + response_dict = self.api.use_item_capture(item_id=berry_id, + encounter_id=encounter_id, + spawn_point_id=self.spawn_point_guid + ) + if response_dict and response_dict['status_code'] is 1 and 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: + for i in range(len(catch_rate)): + if 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: + catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] + success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) + berries_count = berries_count -1 + berry_used = True + self.emit_event( + 'threw_berry', + formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", + data={ + "berry_name": self.item_list[str(berry_id)], + "new_catch_rate": success_percentage + } + ) + else: + if response_dict['status_code'] is 1: + self.emit_event( + 'softban', + level='warning', + formatted='Failed to use berry. You may be softbanned.' + ) + else: + self.emit_event( + 'threw_berry_failed', + formatted='Unknown response when throwing berry: {status_code}.', + data={ + 'status_code': response_dict['status_code'] + } + ) + + # Re-check if berry is used, find a ball for a good capture rate + current_type=pokeball + while current_type < 2: + current_type += 1 + if catch_rate[pokeball-1] < 0.35 and items_stock[current_type] > 0: + pokeball = current_type # use better ball + + # This is to avoid rare case that a berry has ben throwed <0.42 + # and still picking normal pokeball (out of stock) -> error + if items_stock[1] == 0 and items_stock[2] > 0: + pokeball = 2 + + # Add this logic to avoid Pokeball = 0, Great Ball = 0, Ultra Ball = X + # And this logic saves Ultra Balls if it's a weak trash pokemon + if catch_rate[pokeball-1]<0.30 and items_stock[3]>0: + pokeball = 3 + + items_stock[pokeball] -= 1 + success_percentage = '{0:.2f}'.format(catch_rate[pokeball - 1] * 100) + self.emit_event( + 'threw_pokeball', + formatted='Used {pokeball}, with chance {success_percentage} ({count_left} left)', + data={ + 'pokeball': self.item_list[str(pokeball)], + 'success_percentage': success_percentage, + 'count_left': items_stock[pokeball] + } + ) + id_list1 = self.count_pokemon_inventory() - if pokeball is 0: - logger.log( - '[x] Out of pokeballs, switching to farming mode...', 'red') - # Begin searching for pokestops. - self.config.mode = 'farm' - return PokemonCatchWorker.NO_POKEBALLS + reticle_size_parameter = normalized_reticle_size(self.config.catch_randomize_reticle_factor) + spin_modifier_parameter = spin_modifier(self.config.catch_randomize_spin_factor) - balls_stock[pokeball] = balls_stock[pokeball] - 1 - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - logger.log('[x] Using {} (chance: {}%)... ({} left!)'.format( - self.item_list[str(pokeball)], - success_percentage, - balls_stock[pokeball] - )) - - id_list1 = self.count_pokemon_inventory() - self.api.catch_pokemon(encounter_id=encounter_id, - pokeball=pokeball, - normalized_reticle_size=1.950, - spawn_point_guid=spawnpoint_id, - hit_pokemon=1, - spin_modifier=1, - NormalizedHitPosition=1) - response_dict = self.api.call() + response_dict = self.api.catch_pokemon( + encounter_id=encounter_id, + pokeball=pokeball, + normalized_reticle_size=reticle_size_parameter, + spawn_point_id=self.spawn_point_guid, + hit_pokemon=1, + spin_modifier=spin_modifier_parameter, + normalized_hit_position=1 + ) if response_dict and \ - 'responses' in response_dict and \ - 'CATCH_POKEMON' in response_dict['responses'] and \ - 'status' in response_dict['responses']['CATCH_POKEMON']: + 'responses' in response_dict and \ + 'CATCH_POKEMON' in response_dict['responses'] and \ + 'status' in response_dict['responses']['CATCH_POKEMON']: status = response_dict['responses'][ 'CATCH_POKEMON']['status'] if status is 2: - logger.log( - '[-] Attempted to capture {}- failed.. trying again!'.format(pokemon_name), 'red') + self.emit_event( + 'pokemon_fled', + formatted="{pokemon} fled.", + data={'pokemon': pokemon_name} + ) sleep(2) continue if status is 3: - logger.log( - '[x] Oh no! {} vanished! :('.format(pokemon_name), 'red') + self.emit_event( + 'pokemon_vanished', + formatted="{pokemon} vanished!", + data={'pokemon': pokemon_name} + ) + if success_percentage == 100: + self.softban = True if status is 1: - logger.log( - '[x] Captured {}! [CP {}] [IV {}]'.format( - pokemon_name, - cp, - pokemon_potential - ), 'green' + self.bot.metrics.captured_pokemon(pokemon_name, cp, iv_display, pokemon_potential) + + self.emit_event( + 'pokemon_caught', + formatted='Captured {pokemon}! [CP {cp}] [Potential {iv}] [{iv_display}] [+{exp} exp]', + data={ + 'pokemon': pokemon_name, + 'cp': cp, + 'iv': pokemon_potential, + 'iv_display': iv_display, + 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) + } ) + self.bot.softban = False - id_list2 = self.count_pokemon_inventory() + if (self.config.evolve_captured + and (self.config.evolve_captured[0] == 'all' + or pokemon_name in self.config.evolve_captured)): + id_list2 = self.count_pokemon_inventory() + # No need to capture this even for metrics, player stats includes it. + pokemon_to_transfer = list(set(id_list2) - set(id_list1)) - if self.config.evolve_captured: - pokemon_to_transfer = list(Set(id_list2) - Set(id_list1)) - self.api.evolve_pokemon(pokemon_id=pokemon_to_transfer[0]) - response_dict = self.api.call() + # TODO dont throw RuntimeError, do something better + if len(pokemon_to_transfer) == 0: + raise RuntimeError( + 'Trying to evolve 0 pokemons!') + response_dict = self.api.evolve_pokemon(pokemon_id=pokemon_to_transfer[0]) status = response_dict['responses']['EVOLVE_POKEMON']['result'] if status == 1: - logger.log( - '[#] {} has been evolved!'.format(pokemon_name), 'green') + self.emit_event( + 'pokemon_evolved', + formatted="{pokemon} evolved!", + data={'pokemon': pokemon_name} + ) else: - logger.log( - '[x] Failed to evolve {}!'.format(pokemon_name)) - - if self.should_release_pokemon(pokemon_name, cp, pokemon_potential, response_dict): - # Transfering Pokemon - pokemon_to_transfer = list( - Set(id_list2) - Set(id_list1)) - if len(pokemon_to_transfer) == 0: - raise RuntimeError( - 'Trying to transfer 0 pokemons!') - self.transfer_pokemon( - pokemon_to_transfer[0]) - logger.log( - '[#] {} has been exchanged for candy!'.format(pokemon_name), 'green') - else: - logger.log( - '[x] Captured {}! [CP {}]'.format(pokemon_name, cp), 'green') + self.emit_event( + 'pokemon_evolve_fail', + formatted="Failed to evolve {pokemon}!", + data={'pokemon': pokemon_name} + ) break time.sleep(5) - def _transfer_low_cp_pokemon(self, value): - self.api.get_inventory() - response_dict = self.api.call() - self._transfer_all_low_cp_pokemon(value, response_dict) + def count_pokemon_inventory(self): + # don't use cached bot.get_inventory() here + # because we need to have actual information in capture logic + response_dict = self.api.get_inventory() + + id_list = [] + callback = lambda pokemon: id_list.append(pokemon['id']) + self._foreach_pokemon_in_inventory(response_dict, callback) + return id_list - def _transfer_all_low_cp_pokemon(self, value, response_dict): + def _foreach_pokemon_in_inventory(self, response_dict, callback): try: reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) + "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) except KeyError: pass else: for item in response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']: try: reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon"], item) + "inventory_item_data", "pokemon_data"], item) except KeyError: pass else: - pokemon = item['inventory_item_data']['pokemon'] - self._execute_pokemon_transfer(value, pokemon) - time.sleep(1.2) + pokemon = item['inventory_item_data']['pokemon_data'] + if not pokemon.get('is_egg', False): + callback(pokemon) + + def pokemon_potential(self, pokemon_data): + total_iv = 0 + iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] + + for individual_stat in iv_stats: + try: + total_iv += pokemon_data[individual_stat] + except: + pokemon_data[individual_stat] = 0 + continue + + return round((total_iv / 45.0), 2) + + def should_capture_pokemon(self, pokemon_name, cp, iv, response_dict): + catch_config = self._get_catch_config_for(pokemon_name) + cp_iv_logic = catch_config.get('logic') + if not cp_iv_logic: + cp_iv_logic = self._get_catch_config_for('any').get('logic', 'and') + + catch_results = { + 'cp': False, + 'iv': False, + } + + if catch_config.get('never_catch', False): + return False - def _execute_pokemon_transfer(self, value, pokemon): - if 'cp' in pokemon and pokemon['cp'] < value: - self.api.release_pokemon(pokemon_id=pokemon['id']) - response_dict = self.api.call() + if catch_config.get('always_catch', False): + return True - def transfer_pokemon(self, pid): - self.api.release_pokemon(pokemon_id=pid) - response_dict = self.api.call() + catch_cp = catch_config.get('catch_above_cp', 0) + if cp > catch_cp: + catch_results['cp'] = True - def count_pokemon_inventory(self): - self.api.get_inventory() - response_dict = self.api.call() - id_list = [] - return self.counting_pokemon(response_dict, id_list) + catch_iv = catch_config.get('catch_above_iv', 0) + if iv > catch_iv: + catch_results['iv'] = True - def counting_pokemon(self, response_dict, id_list): - try: - reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) - except KeyError: - pass - else: - for item in response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data"], item) - except KeyError: - pass - else: - pokemon = item['inventory_item_data']['pokemon_data'] - if pokemon.get('is_egg', False): - continue - id_list.append(pokemon['id']) + logic_to_function = { + 'or': lambda x, y: x or y, + 'and': lambda x, y: x and y + } - return id_list + return logic_to_function[cp_iv_logic](*catch_results.values()) - def should_release_pokemon(self, pokemon_name, cp, iv, response_dict): - if self._check_always_capture_exception_for(pokemon_name): - return False + def _get_catch_config_for(self, pokemon): + catch_config = self.config.catch.get(pokemon) + if not catch_config: + catch_config = self.config.catch.get('any') + return catch_config + + def create_encounter_api_call(self): + encounter_id = self.pokemon['encounter_id'] + player_latitude = self.pokemon['latitude'] + player_longitude = self.pokemon['longitude'] + + request = self.api.create_request() + if 'spawn_point_id' in self.pokemon: + spawn_point_id = self.pokemon['spawn_point_id'] + self.spawn_point_guid = spawn_point_id + self.response_key = 'ENCOUNTER' + self.response_status_key = 'status' + request.encounter( + encounter_id=encounter_id, + spawn_point_id=spawn_point_id, + player_latitude=player_latitude, + player_longitude=player_longitude + ) else: - release_config = self._get_release_config_for(pokemon_name) - cp_iv_logic = release_config.get('cp_iv_logic') - if not cp_iv_logic: - cp_iv_logic = self._get_release_config_for('any').get('cp_iv_logic', 'and') - - release_results = { - 'cp': False, - 'iv': False, - } - - if 'release_under_cp' in release_config: - min_cp = release_config['release_under_cp'] - if cp < min_cp: - release_results['cp'] = True - - if 'release_under_iv' in release_config: - min_iv = release_config['release_under_iv'] - if iv < min_iv: - release_results['iv'] = True - - if release_config.get('always_release'): - return True - - logic_to_function = { - 'or': lambda x, y: x or y, - 'and': lambda x, y: x and y - } - - #logger.log( - # "[x] Release config for {}: CP {} {} IV {}".format( - # pokemon_name, - # min_cp, - # cp_iv_logic, - # min_iv - # ), 'yellow' - #) - - return logic_to_function[cp_iv_logic](*release_results.values()) - - def _get_release_config_for(self, pokemon): - release_config = self.config.release_config.get(pokemon) - if not release_config: - release_config = self.config.release_config['any'] - return release_config - - def _get_exceptions(self): - exceptions = self.config.release_config.get('exceptions') - if not exceptions: - return None - return exceptions - - def _get_always_capture_list(self): - exceptions = self._get_exceptions() - if not exceptions: - return [] - always_capture_list = exceptions['always_capture'] - if not always_capture_list: - return [] - return always_capture_list - - def _check_always_capture_exception_for(self, pokemon_name): - always_capture_list = self._get_always_capture_list() - if not always_capture_list: - return False + fort_id = self.pokemon['fort_id'] + self.spawn_point_guid = fort_id + self.response_key = 'DISK_ENCOUNTER' + self.response_status_key = 'result' + request.disk_encounter( + encounter_id=encounter_id, + fort_id=fort_id, + player_latitude=player_latitude, + player_longitude=player_longitude + ) + return request.call() + + def check_vip_pokemon(self,pokemon, cp, iv): + + vip_name = self.config.vips.get(pokemon) + if vip_name == {}: + return True else: - for pokemon in always_capture_list: - if pokemon_name == str(pokemon): - return True - return False + catch_config = self.config.vips.get("any") + if not catch_config: + return False + cp_iv_logic = catch_config.get('logic', 'or') + catch_results = { + 'cp': False, + 'iv': False, + } + + catch_cp = catch_config.get('catch_above_cp', 0) + if cp > catch_cp: + catch_results['cp'] = True + catch_iv = catch_config.get('catch_above_iv', 0) + if iv > catch_iv: + catch_results['iv'] = True + logic_to_function = { + 'or': lambda x, y: x or y, + 'and': lambda x, y: x and y + } + return logic_to_function[cp_iv_logic](*catch_results.values()) diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py new file mode 100644 index 0000000000..c28b2749b1 --- /dev/null +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -0,0 +1,63 @@ +import json +import os +from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.tree_config_builder import ConfigException + +class RecycleItems(BaseTask): + def initialize(self): + self.item_filter = self.config.get('item_filter', {}) + self._validate_item_filter() + + def _validate_item_filter(self): + item_list = json.load(open(os.path.join('data', 'items.json'))) + for config_item_name, bag_count in self.item_filter.iteritems(): + if config_item_name not in item_list.viewvalues(): + if config_item_name not in item_list: + raise ConfigException("item {} does not exist, spelling mistake? (check for valid item names in data/items.json)".format(config_item_name)) + + def work(self): + self.bot.latest_inventory = None + item_count_dict = self.bot.item_inventory_count('all') + + for item_id, bag_count in item_count_dict.iteritems(): + item_name = self.bot.item_list[str(item_id)] + id_filter = self.item_filter.get(item_name, 0) + if id_filter is not 0: + id_filter_keep = id_filter.get('keep', 20) + else: + id_filter = self.item_filter.get(str(item_id), 0) + if id_filter is not 0: + id_filter_keep = id_filter.get('keep', 20) + + bag_count = self.bot.item_inventory_count(item_id) + if (item_name in self.item_filter or str(item_id) in self.item_filter) and bag_count > id_filter_keep: + items_recycle_count = bag_count - id_filter_keep + response_dict_recycle = self.send_recycle_item_request(item_id=item_id, count=items_recycle_count) + result = response_dict_recycle.get('responses', {}).get('RECYCLE_INVENTORY_ITEM', {}).get('result', 0) + + if result == 1: # Request success + self.emit_event( + 'item_discarded', + formatted='Discarded {amount}x {item} (maximum {maximum}).', + data={ + 'amount': str(items_recycle_count), + 'item': item_name, + 'maximum': str(id_filter_keep) + } + ) + else: + self.emit_event( + 'item_discard_fail', + formatted="Failed to discard {item}", + data={ + 'item': item_name + } + ) + + def send_recycle_item_request(self, item_id, count): + # Example of good request response + #{'responses': {'RECYCLE_INVENTORY_ITEM': {'result': 1, 'new_count': 46}}, 'status_code': 1, 'auth_ticket': {'expire_timestamp_ms': 1469306228058L, 'start': '/HycFyfrT4t2yB2Ij+yoi+on778aymMgxY6RQgvrGAfQlNzRuIjpcnDd5dAxmfoTqDQrbz1m2dGqAIhJ+eFapg==', 'end': 'f5NOZ95a843tgzprJo4W7Q=='}, 'request_id': 8145806132888207460L} + return self.bot.api.recycle_inventory_item( + item_id=item_id, + count=count + ) diff --git a/pokemongo_bot/cell_workers/seen_fort_worker.py b/pokemongo_bot/cell_workers/seen_fort_worker.py deleted file mode 100644 index a1faafa66d..0000000000 --- a/pokemongo_bot/cell_workers/seen_fort_worker.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- coding: utf-8 -*- - -import json -import time -from math import radians, sqrt, sin, cos, atan2 -from pgoapi.utilities import f2i, h2f -from utils import print_green, print_yellow, print_red, format_time -from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot import logger - - -class SeenFortWorker(object): - def __init__(self, fort, bot): - self.fort = fort - self.api = bot.api - self.bot = bot - self.position = bot.position - self.config = bot.config - self.item_list = bot.item_list - self.rest_time = 50 - self.stepper = bot.stepper - - def work(self): - lat = self.fort['latitude'] - lng = self.fort['longitude'] - - self.api.fort_details(fort_id=self.fort['id'], - latitude=lat, - longitude=lng) - response_dict = self.api.call() - if 'responses' in response_dict \ - and'FORT_DETAILS' in response_dict['responses'] \ - and 'name' in response_dict['responses']['FORT_DETAILS']: - fort_details = response_dict['responses']['FORT_DETAILS'] - fort_name = fort_details['name'].encode('utf8', 'replace') - else: - fort_name = 'Unknown' - logger.log('[#] Now at Pokestop: ' + fort_name + ' - Spinning...', - 'yellow') - sleep(2) - self.api.fort_search(fort_id=self.fort['id'], - fort_latitude=lat, - fort_longitude=lng, - player_latitude=f2i(self.position[0]), - player_longitude=f2i(self.position[1])) - response_dict = self.api.call() - if 'responses' in response_dict and \ - 'FORT_SEARCH' in response_dict['responses']: - - spin_details = response_dict['responses']['FORT_SEARCH'] - if spin_details['result'] == 1: - logger.log("[+] Loot: ", 'green') - experience_awarded = spin_details.get('experience_awarded', - False) - if experience_awarded: - logger.log("[+] " + str(experience_awarded) + " xp", - 'green') - - items_awarded = spin_details.get('items_awarded', False) - if items_awarded: - tmp_count_items = {} - for item in items_awarded: - item_id = item['item_id'] - if not item_id in tmp_count_items: - tmp_count_items[item_id] = item['item_count'] - else: - tmp_count_items[item_id] += item['item_count'] - - for item_id, item_count in tmp_count_items.iteritems(): - item_name = self.item_list[str(item_id)] - - logger.log("[+] " + str(item_count) + - "x " + item_name + - " (Total: " + str(self.bot.item_inventory_count(item_id)) + ")", 'green') - - # RECYCLING UNWANTED ITEMS - if str(item_id) in self.config.item_filter: - logger.log("[+] Recycling " + str(item_count) + "x " + item_name + "...", 'green') - #RECYCLE_INVENTORY_ITEM - response_dict_recycle = self.bot.drop_item(item_id=item_id, count=item_count) - - if response_dict_recycle and \ - 'responses' in response_dict_recycle and \ - 'RECYCLE_INVENTORY_ITEM' in response_dict_recycle['responses'] and \ - 'result' in response_dict_recycle['responses']['RECYCLE_INVENTORY_ITEM']: - result = response_dict_recycle['responses']['RECYCLE_INVENTORY_ITEM']['result'] - if result is 1: # Request success - logger.log("[+] Recycling success", 'green') - else: - logger.log("[+] Recycling failed!", 'red') - else: - logger.log("[#] Nothing found.", 'yellow') - - pokestop_cooldown = spin_details.get( - 'cooldown_complete_timestamp_ms') - if pokestop_cooldown: - seconds_since_epoch = time.time() - logger.log('[#] PokeStop on cooldown. Time left: ' + str( - format_time((pokestop_cooldown / 1000) - - seconds_since_epoch))) - - if not items_awarded and not experience_awarded and not pokestop_cooldown: - message = ( - 'Stopped at Pokestop and did not find experience, items ' - 'or information about the stop cooldown. You are ' - 'probably softbanned. Try to play on your phone, ' - 'if pokemons always ran away and you find nothing in ' - 'PokeStops you are indeed softbanned. Please try again ' - 'in a few hours.') - raise RuntimeError(message) - elif spin_details['result'] == 2: - logger.log("[#] Pokestop out of range") - elif spin_details['result'] == 3: - pokestop_cooldown = spin_details.get( - 'cooldown_complete_timestamp_ms') - if pokestop_cooldown: - seconds_since_epoch = time.time() - logger.log('[#] PokeStop on cooldown. Time left: ' + str( - format_time((pokestop_cooldown / 1000) - - seconds_since_epoch))) - elif spin_details['result'] == 4: - print_red("[#] Inventory is full, switching to catch mode...") - self.config.mode = 'poke' - - if 'chain_hack_sequence_number' in response_dict['responses'][ - 'FORT_SEARCH']: - time.sleep(2) - return response_dict['responses']['FORT_SEARCH'][ - 'chain_hack_sequence_number'] - else: - print_yellow('[#] may search too often, lets have a rest') - return 11 - sleep(8) - return 0 - - @staticmethod - def closest_fort(current_lat, current_long, forts): - print x diff --git a/pokemongo_bot/cell_workers/sleep_schedule.py b/pokemongo_bot/cell_workers/sleep_schedule.py new file mode 100644 index 0000000000..daaf0b8f1e --- /dev/null +++ b/pokemongo_bot/cell_workers/sleep_schedule.py @@ -0,0 +1,108 @@ +from datetime import datetime, timedelta +from time import sleep +from random import uniform +from pokemongo_bot.cell_workers.base_task import BaseTask + + +class SleepSchedule(BaseTask): + """Pauses the execution of the bot every day for some time + + Simulates the user going to sleep every day for some time, the sleep time + and the duration is changed every day by a random offset defined in the + config file + Example Config: + { + "type": "SleepSchedule", + "config": { + "time": "12:00", + "duration":"5:30", + "time_random_offset": "00:30", + "duration_random_offset": "00:30" + } + } + time: (HH:MM) local time that the bot should sleep + duration: (HH:MM) the duration of sleep + time_random_offset: (HH:MM) random offset of time that the sleep will start + for this example the possible start time is 11:30-12:30 + duration_random_offset: (HH:MM) random offset of duration of sleep + for this example the possible duration is 5:00-6:00 + """ + + LOG_INTERVAL_SECONDS = 600 + SCHEDULING_MARGIN = timedelta(minutes=10) # Skip if next sleep is RESCHEDULING_MARGIN from now + + def initialize(self): + # self.bot.event_manager.register_event('sleeper_scheduled', parameters=('datetime',)) + self._process_config() + self._schedule_next_sleep() + + def work(self): + if datetime.now() >= self._next_sleep: + self._sleep() + self._schedule_next_sleep() + self.bot.login() + + def _process_config(self): + self.time = datetime.strptime(self.config.get('time', '01:00'), '%H:%M') + + # Using datetime for easier stripping of timedeltas + duration = datetime.strptime(self.config.get('duration', '07:00'), '%H:%M') + self.duration = int(timedelta(hours=duration.hour, minutes=duration.minute).total_seconds()) + + time_random_offset = datetime.strptime(self.config.get('time_random_offset', '01:00'), '%H:%M') + self.time_random_offset = int( + timedelta( + hours=time_random_offset.hour, minutes=time_random_offset.minute).total_seconds()) + + duration_random_offset = datetime.strptime(self.config.get('duration_random_offset', '00:30'), '%H:%M') + self.duration_random_offset = int( + timedelta( + hours=duration_random_offset.hour, minutes=duration_random_offset.minute).total_seconds()) + + def _schedule_next_sleep(self): + self._next_sleep = self._get_next_sleep_schedule() + self._next_duration = self._get_next_duration() + self.emit_event( + 'next_sleep', + formatted="Next sleep at {time}", + data={ + 'time': str(self._next_sleep) + } + ) + + def _get_next_sleep_schedule(self): + now = datetime.now() + self.SCHEDULING_MARGIN + next_time = now.replace(hour=self.time.hour, minute=self.time.minute) + + next_time += timedelta(seconds=self._get_random_offset(self.time_random_offset)) + + # If sleep time is passed add one day + if next_time <= now: + next_time += timedelta(days=1) + + return next_time + + def _get_next_duration(self): + duration = self.duration + self._get_random_offset(self.duration_random_offset) + return duration + + def _get_random_offset(self, max_offset): + offset = uniform(-max_offset, max_offset) + return int(offset) + + def _sleep(self): + sleep_to_go = self._next_duration + self.emit_event( + 'bot_sleep', + formatted="Sleeping for {time_in_seconds}", + data={ + 'time_in_seconds': sleep_to_go + } + ) + while sleep_to_go > 0: + if sleep_to_go < self.LOG_INTERVAL_SECONDS: + sleep(sleep_to_go) + sleep_to_go = 0 + else: + sleep(self.LOG_INTERVAL_SECONDS) + sleep_to_go -= self.LOG_INTERVAL_SECONDS diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py new file mode 100644 index 0000000000..9572008241 --- /dev/null +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import time + +from pgoapi.utilities import f2i + +from pokemongo_bot.constants import Constants +from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.cell_workers.base_task import BaseTask +from utils import distance, format_time, fort_details + + +class SpinFort(BaseTask): + def should_run(self): + if not self.bot.has_space_for_loot(): + self.emit_event( + 'inventory_full', + formatted="Not moving to any forts as there aren't enough space. You might want to change your config to recycle more items if this message appears consistently." + ) + return False + return True + + def work(self): + fort = self.get_fort_in_range() + + if not self.should_run() or fort is None: + return WorkerResult.SUCCESS + + lat = fort['latitude'] + lng = fort['longitude'] + + details = fort_details(self.bot, fort['id'], lat, lng) + fort_name = details.get('name', 'Unknown') + + response_dict = self.bot.api.fort_search( + fort_id=fort['id'], + fort_latitude=lat, + fort_longitude=lng, + player_latitude=f2i(self.bot.position[0]), + player_longitude=f2i(self.bot.position[1]) + ) + if 'responses' in response_dict and \ + 'FORT_SEARCH' in response_dict['responses']: + + spin_details = response_dict['responses']['FORT_SEARCH'] + spin_result = spin_details.get('result', -1) + if spin_result == 1: + self.bot.softban = False + experience_awarded = spin_details.get('experience_awarded', 0) + items_awarded = spin_details.get('items_awarded', {}) + if items_awarded: + self.bot.latest_inventory = None + tmp_count_items = {} + for item in items_awarded: + item_id = item['item_id'] + item_name = self.bot.item_list[str(item_id)] + if not item_name in tmp_count_items: + tmp_count_items[item_name] = item['item_count'] + else: + tmp_count_items[item_name] += item['item_count'] + + if experience_awarded or items_awarded: + self.emit_event( + 'spun_pokestop', + formatted="Spun pokestop {pokestop}. Experience awarded: {exp}. Items awarded: {items}", + data={ + 'pokestop': fort_name, + 'exp': experience_awarded, + 'items': tmp_count_items + } + ) + else: + self.emit_event( + 'pokestop_empty', + formatted='Found nothing in pokestop {pokestop}.', + data={'pokestop': fort_name} + ) + pokestop_cooldown = spin_details.get( + 'cooldown_complete_timestamp_ms') + self.bot.fort_timeouts.update({fort["id"]: pokestop_cooldown}) + self.bot.recent_forts = self.bot.recent_forts[1:] + [fort['id']] + elif spin_result == 2: + self.emit_event( + 'pokestop_out_of_range', + formatted="Pokestop {pokestop} out of range.", + data={'pokestop': fort_name} + ) + elif spin_result == 3: + pokestop_cooldown = spin_details.get( + 'cooldown_complete_timestamp_ms') + if pokestop_cooldown: + self.bot.fort_timeouts.update({fort["id"]: pokestop_cooldown}) + seconds_since_epoch = time.time() + minutes_left = format_time( + (pokestop_cooldown / 1000) - seconds_since_epoch + ) + self.emit_event( + 'pokestop_on_cooldown', + formatted="Pokestop {pokestop} on cooldown. Time left: {minutes_left}.", + data={'pokestop': fort_name, 'minutes_left': minutes_left} + ) + elif spin_result == 4: + self.emit_event( + 'inventory_full', + formatted="Inventory is full!" + ) + else: + self.emit_event( + 'unknown_spin_result', + formatted="Unknown spint result {status_code}", + data={'status_code': str(spin_result)} + ) + if 'chain_hack_sequence_number' in response_dict['responses'][ + 'FORT_SEARCH']: + time.sleep(2) + return response_dict['responses']['FORT_SEARCH'][ + 'chain_hack_sequence_number'] + else: + self.emit_event( + 'pokestop_searching_too_often', + formatted="Possibly searching too often, take a rest." + ) + if spin_result == 1 and not items_awarded and not experience_awarded and not pokestop_cooldown: + self.bot.softban = True + self.emit_event( + 'softban', + formatted='Probably got softban.' + ) + else: + self.bot.fort_timeouts[fort["id"]] = (time.time() + 300) * 1000 # Don't spin for 5m + return 11 + sleep(2) + return 0 + + def get_fort_in_range(self): + forts = self.bot.get_forts(order_by_distance=True) + + forts = filter(lambda x: x["id"] not in self.bot.fort_timeouts, forts) + + if len(forts) == 0: + return None + + fort = forts[0] + + distance_to_fort = distance( + self.bot.position[0], + self.bot.position[1], + fort['latitude'], + fort['longitude'] + ) + + if distance_to_fort <= Constants.MAX_DISTANCE_FORT_IS_REACHABLE: + return fort + + return None diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py new file mode 100644 index 0000000000..70c5939c58 --- /dev/null +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -0,0 +1,233 @@ +import json + +from pokemongo_bot.human_behaviour import action_delay +from pokemongo_bot.cell_workers.base_task import BaseTask + + +class TransferPokemon(BaseTask): + def work(self): + pokemon_groups = self._release_pokemon_get_groups() + for pokemon_id in pokemon_groups: + group = pokemon_groups[pokemon_id] + + if len(group) > 0: + pokemon_name = self.bot.pokemon_list[pokemon_id - 1]['Name'] + keep_best, keep_best_cp, keep_best_iv = self._validate_keep_best_config(pokemon_name) + + if keep_best: + best_pokemon_ids = set() + order_criteria = 'none' + if keep_best_cp >= 1: + cp_limit = keep_best_cp + best_cp_pokemons = sorted(group, key=lambda x: (x['cp'], x['iv']), reverse=True)[:cp_limit] + best_pokemon_ids = set(pokemon['pokemon_data']['id'] for pokemon in best_cp_pokemons) + order_criteria = 'cp' + + if keep_best_iv >= 1: + iv_limit = keep_best_iv + best_iv_pokemons = sorted(group, key=lambda x: (x['iv'], x['cp']), reverse=True)[:iv_limit] + best_pokemon_ids |= set(pokemon['pokemon_data']['id'] for pokemon in best_iv_pokemons) + if order_criteria == 'cp': + order_criteria = 'cp and iv' + else: + order_criteria = 'iv' + + # remove best pokemons from all pokemons array + all_pokemons = group + best_pokemons = [] + for best_pokemon_id in best_pokemon_ids: + for pokemon in all_pokemons: + if best_pokemon_id == pokemon['pokemon_data']['id']: + all_pokemons.remove(pokemon) + best_pokemons.append(pokemon) + + if best_pokemons and all_pokemons: + self.emit_event( + 'keep_best_release', + formatted="Keeping best {amount} {pokemon}, based on {criteria}", + data={ + 'amount': len(best_pokemons), + 'pokemon': pokemon_name, + 'criteria': order_criteria + } + ) + + transfer_pokemons = [pokemon for pokemon in all_pokemons + if self.should_release_pokemon(pokemon_name, + pokemon['cp'], + pokemon['iv'], + True)] + + if transfer_pokemons: + for pokemon in transfer_pokemons: + self.release_pokemon(pokemon_name, pokemon['cp'], pokemon['iv'], pokemon['pokemon_data']['id']) + else: + group = sorted(group, key=lambda x: x['cp'], reverse=True) + for item in group: + pokemon_cp = item['cp'] + pokemon_potential = item['iv'] + + if self.should_release_pokemon(pokemon_name, pokemon_cp, pokemon_potential): + self.release_pokemon(pokemon_name, item['cp'], item['iv'], item['pokemon_data']['id']) + + def _release_pokemon_get_groups(self): + pokemon_groups = {} + request = self.bot.api.create_request() + request.get_player() + request.get_inventory() + inventory_req = request.call() + + if inventory_req.get('responses', False) is False: + return pokemon_groups + + inventory_dict = inventory_req['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] + + user_web_inventory = 'web/inventory-%s.json' % (self.bot.config.username) + with open(user_web_inventory, 'w') as outfile: + json.dump(inventory_dict, outfile) + + for pokemon in inventory_dict: + try: + reduce(dict.__getitem__, [ + "inventory_item_data", "pokemon_data", "pokemon_id" + ], pokemon) + except KeyError: + continue + + pokemon_data = pokemon['inventory_item_data']['pokemon_data'] + + # pokemon in fort, so we cant transfer it + if 'deployed_fort_id' in pokemon_data and pokemon_data['deployed_fort_id']: + continue + + # favorite pokemon can't transfer in official game client + if pokemon_data.get('favorite', 0) is 1: + continue + + group_id = pokemon_data['pokemon_id'] + group_pokemon_cp = pokemon_data['cp'] + group_pokemon_iv = self.get_pokemon_potential(pokemon_data) + + if group_id not in pokemon_groups: + pokemon_groups[group_id] = [] + + pokemon_groups[group_id].append({ + 'cp': group_pokemon_cp, + 'iv': group_pokemon_iv, + 'pokemon_data': pokemon_data + }) + + return pokemon_groups + + def get_pokemon_potential(self, pokemon_data): + total_iv = 0 + iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] + for individual_stat in iv_stats: + try: + total_iv += pokemon_data[individual_stat] + except Exception: + continue + return round((total_iv / 45.0), 2) + + def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): + release_config = self._get_release_config_for(pokemon_name) + + if (keep_best_mode + and not release_config.has_key('never_release') + and not release_config.has_key('always_release') + and not release_config.has_key('release_below_cp') + and not release_config.has_key('release_below_iv')): + return True + + cp_iv_logic = release_config.get('logic') + if not cp_iv_logic: + cp_iv_logic = self._get_release_config_for('any').get('logic', 'and') + + release_results = { + 'cp': False, + 'iv': False, + } + + if release_config.get('never_release', False): + return False + + if release_config.get('always_release', False): + return True + + release_cp = release_config.get('release_below_cp', 0) + if cp < release_cp: + release_results['cp'] = True + + release_iv = release_config.get('release_below_iv', 0) + if iv < release_iv: + release_results['iv'] = True + + logic_to_function = { + 'or': lambda x, y: x or y, + 'and': lambda x, y: x and y + } + + if logic_to_function[cp_iv_logic](*release_results.values()): + self.emit_event( + 'future_pokemon_release', + formatted="Releasing {pokemon} (CP {cp}/IV {iv}) based on rule: CP < {below_cp} {cp_iv_logic} IV < {below_iv}", + data={ + 'pokemon': pokemon_name, + 'cp': cp, + 'iv': iv, + 'below_cp': release_cp, + 'cp_iv_logic': cp_iv_logic.upper(), + 'below_iv': release_iv + } + ) + + return logic_to_function[cp_iv_logic](*release_results.values()) + + def release_pokemon(self, pokemon_name, cp, iv, pokemon_id): + response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon_id) + self.emit_event( + 'pokemon_release', + formatted='Exchanged {pokemon} [CP {cp}] [IV {iv}] for candy.', + data={ + 'pokemon': pokemon_name, + 'cp': cp, + 'iv': iv + } + ) + action_delay(self.bot.config.action_wait_min, self.bot.config.action_wait_max) + + def _get_release_config_for(self, pokemon): + release_config = self.bot.config.release.get(pokemon) + if not release_config: + release_config = self.bot.config.release.get('any') + if not release_config: + release_config = {} + return release_config + + def _validate_keep_best_config(self, pokemon_name): + keep_best = False + + release_config = self._get_release_config_for(pokemon_name) + + keep_best_cp = release_config.get('keep_best_cp', 0) + keep_best_iv = release_config.get('keep_best_iv', 0) + + if keep_best_cp or keep_best_iv: + keep_best = True + try: + keep_best_cp = int(keep_best_cp) + except ValueError: + keep_best_cp = 0 + + try: + keep_best_iv = int(keep_best_iv) + except ValueError: + keep_best_iv = 0 + + if keep_best_cp < 0 or keep_best_iv < 0: + keep_best = False + + if keep_best_cp == 0 and keep_best_iv == 0: + keep_best = False + + return keep_best, keep_best_cp, keep_best_iv diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py new file mode 100644 index 0000000000..911d2efd4d --- /dev/null +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -0,0 +1,233 @@ +import ctypes +from sys import stdout, platform as _platform +from datetime import datetime, timedelta + +from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.tree_config_builder import ConfigException + +class UpdateTitleStats(BaseTask): + """ + Periodically updates the terminal title to display stats about the bot. + + Fetching some stats requires making API calls. If you're concerned about the amount of calls + your bot is making, don't enable this worker. + + Example config : + { + "type": "UpdateTitleStats", + "config": { + "min_interval": 10, + "stats": ["uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"] + } + } + + Available stats : + - uptime : The bot uptime. + - km_walked : The kilometers walked since the bot started. + - level : The current character's level. + - level_completion : The current level experience, the next level experience and the completion + percentage. + - level_stats : Puts together the current character's level and its completion. + - xp_per_hour : The estimated gain of experience per hour. + - xp_earned : The experience earned since the bot started. + - stops_visited : The number of visited stops. + - pokemon_encountered : The number of encountered pokemon. + - pokemon_caught : The number of caught pokemon. + - pokemon_released : The number of released pokemon. + - pokemon_evolved : The number of evolved pokemon. + - pokemon_unseen : The number of pokemon never seen before. + - pokemon_stats : Puts together the pokemon encountered, caught, released, evolved and unseen. + - pokeballs_thrown : The number of thrown pokeballs. + - stardust_earned : The number of earned stardust since the bot started. + - highest_cp_pokemon : The caught pokemon with the highest CP since the bot started. + - most_perfect_pokemon : The most perfect caught pokemon since the bot started. + + min_interval : The minimum interval at which the title is updated, + in seconds (defaults to 10 seconds). + The update interval cannot be accurate as workers run synchronously. + stats : An array of stats to display and their display order (implicitly), + see available stats above. + """ + + DEFAULT_MIN_INTERVAL = 10 + DEFAULT_DISPLAYED_STATS = [] + + def __init__(self, bot, config): + """ + Initializes the worker. + :param bot: The bot instance. + :type bot: PokemonGoBot + :param config: The task configuration. + :type config: dict + """ + super(UpdateTitleStats, self).__init__(bot, config) + + self.next_update = None + self.min_interval = self.DEFAULT_MIN_INTERVAL + self.displayed_stats = self.DEFAULT_DISPLAYED_STATS + + self._process_config() + + def initialize(self): + pass + + def work(self): + """ + Updates the title if necessary. + :return: Always returns WorkerResult.SUCCESS. + :rtype: WorkerResult + """ + if not self._should_display(): + return WorkerResult.SUCCESS + title = self._get_stats_title(self._get_player_stats()) + # If title is empty, it couldn't be generated. + if not title: + return WorkerResult.SUCCESS + self._update_title(title, _platform) + return WorkerResult.SUCCESS + + def _should_display(self): + """ + Returns a value indicating whether the title should be updated. + :return: True if the title should be updated; otherwise, False. + :rtype: bool + """ + return self.next_update is None or datetime.now() >= self.next_update + + def _update_title(self, title, platform): + """ + Updates the window title using different methods, according to the given platform + :param title: The new window title. + :type title: string + :param platform: The platform string. + :type platform: string + :return: Nothing. + :rtype: None + :raise: RuntimeError: When the given platform isn't supported. + """ + if platform == "linux" or platform == "linux2"\ + or platform == "cygwin": + stdout.write("\x1b]2;{}\x07".format(title)) + elif platform == "darwin": + stdout.write("\033]0;{}\007".format(title)) + elif platform == "win32": + ctypes.windll.kernel32.SetConsoleTitleA(title) + else: + raise RuntimeError("unsupported platform '{}'".format(platform)) + + self.next_update = datetime.now() + timedelta(seconds=self.min_interval) + + def _process_config(self): + """ + Fetches the configuration for this worker and stores the values internally. + :return: Nothing. + :rtype: None + """ + self.min_interval = int(self.config.get('min_interval', self.DEFAULT_MIN_INTERVAL)) + self.displayed_stats = self.config.get('stats', self.DEFAULT_DISPLAYED_STATS) + + def _get_stats_title(self, player_stats): + """ + Generates a stats string with the given player stats according to the configuration. + :return: A string containing human-readable stats, ready to be displayed. + :rtype: string + """ + # No player stats available, won't be able to gather all informations. + if player_stats is None: + return '' + # No stats to display, avoid any useless overhead. + if not self.displayed_stats: + return '' + + # Gather stats values. + metrics = self.bot.metrics + metrics.capture_stats() + runtime = metrics.runtime() + distance_travelled = metrics.distance_travelled() + current_level = int(player_stats.get('level', 0)) + prev_level_xp = int(player_stats.get('prev_level_xp', 0)) + next_level_xp = int(player_stats.get('next_level_xp', 0)) + experience = int(player_stats.get('experience', 0)) + current_level_xp = experience - prev_level_xp + whole_level_xp = next_level_xp - prev_level_xp + level_completion_percentage = int((current_level_xp * 100) / whole_level_xp) + experience_per_hour = int(metrics.xp_per_hour()) + xp_earned = metrics.xp_earned() + stops_visited = metrics.visits['latest'] - metrics.visits['start'] + pokemon_encountered = metrics.num_encounters() + pokemon_caught = metrics.num_captures() + pokemon_released = metrics.releases + pokemon_evolved = metrics.num_evolutions() + pokemon_unseen = metrics.num_new_mons() + pokeballs_thrown = metrics.num_throws() + stardust_earned = metrics.earned_dust() + highest_cp_pokemon = metrics.highest_cp['desc'] + if not highest_cp_pokemon: + highest_cp_pokemon = "None" + most_perfect_pokemon = metrics.most_perfect['desc'] + if not most_perfect_pokemon: + most_perfect_pokemon = "None" + + # Create stats strings. + available_stats = { + 'uptime': 'Uptime : {}'.format(runtime), + 'km_walked': '{:,.2f}km walked'.format(distance_travelled), + 'level': 'Level {}'.format(current_level), + 'level_completion': '{:,} / {:,} XP ({}%)'.format(current_level_xp, whole_level_xp, + level_completion_percentage), + 'level_stats': 'Level {} ({:,} / {:,}, {}%)'.format(current_level, current_level_xp, + whole_level_xp, + level_completion_percentage), + 'xp_per_hour': '{:,} XP/h'.format(experience_per_hour), + 'xp_earned': '+{:,} XP'.format(xp_earned), + 'stops_visited': 'Visited {:,} stops'.format(stops_visited), + 'pokemon_encountered': 'Encountered {:,} pokemon'.format(pokemon_encountered), + 'pokemon_caught': 'Caught {:,} pokemon'.format(pokemon_caught), + 'pokemon_released': 'Released {:,} pokemon'.format(pokemon_released), + 'pokemon_evolved': 'Evolved {:,} pokemon'.format(pokemon_evolved), + 'pokemon_unseen': 'Encountered {} new pokemon'.format(pokemon_unseen), + 'pokemon_stats': 'Encountered {:,} pokemon, {:,} caught, {:,} released, {:,} evolved, ' + '{} never seen before'.format(pokemon_encountered, pokemon_caught, + pokemon_released, pokemon_evolved, + pokemon_unseen), + 'pokeballs_thrown': 'Threw {:,} pokeballs'.format(pokeballs_thrown), + 'stardust_earned': 'Earned {:,} Stardust'.format(stardust_earned), + 'highest_cp_pokemon': 'Highest CP pokemon : {}'.format(highest_cp_pokemon), + 'most_perfect_pokemon': 'Most perfect pokemon : {}'.format(most_perfect_pokemon), + } + + def get_stat(stat): + """ + Fetches a stat string from the available stats dictionary. + :param stat: The stat name. + :type stat: string + :return: The generated stat string. + :rtype: string + :raise: ConfigException: When the provided stat string isn't in the available stats + dictionary. + """ + if stat not in available_stats: + raise ConfigException("stat '{}' isn't available for displaying".format(stat)) + return available_stats[stat] + + # Map stats the user wants to see to available stats and join them with pipes. + title = ' | '.join(map(get_stat, self.displayed_stats)) + + return title + + def _get_player_stats(self): + """ + Helper method parsing the bot inventory object and returning the player stats object. + :return: The player stats object. + :rtype: dict + """ + inventory_items = self.bot.get_inventory() \ + .get('responses', {}) \ + .get('GET_INVENTORY', {}) \ + .get('inventory_delta', {}) \ + .get('inventory_items', {}) + return next((x["inventory_item_data"]["player_stats"] + for x in inventory_items + if x.get("inventory_item_data", {}).get("player_stats", {})), + None) diff --git a/pokemongo_bot/cell_workers/utils.py b/pokemongo_bot/cell_workers/utils.py index bd31375ccc..946c757b16 100644 --- a/pokemongo_bot/cell_workers/utils.py +++ b/pokemongo_bot/cell_workers/utils.py @@ -1,10 +1,49 @@ # -*- coding: utf-8 -*- import struct -from math import cos, asin, sqrt +from math import asin, atan, cos, exp, log, pi, sin, sqrt, tan + from colorama import init +from networkx.algorithms.clique import find_cliques + +import networkx as nx +import numpy as np + init() +TIME_PERIODS = ( + (60, 'minute'), + (3600, 'hour'), + (86400, 'day'), + (86400*7, 'week') +) + +FORT_CACHE = {} +def fort_details(bot, fort_id, latitude, longitude): + """ + Lookup fort metadata and (if possible) serve from cache. + """ + + if fort_id not in FORT_CACHE: + """ + Lookup the fort details and cache the response for future use. + """ + request = bot.api.create_request() + request.fort_details(fort_id=fort_id, latitude=latitude, longitude=longitude) + try: + response_dict = request.call() + FORT_CACHE[fort_id] = response_dict['responses']['FORT_DETAILS'] + except Exception: + pass + + # Just to avoid KeyErrors + return FORT_CACHE.get(fort_id, {}) + +def encode(cellid): + output = [] + encoder._VarintEncoder()(output.append, cellid) + return ''.join(output) + def distance(lat1, lon1, lat2, lon2): p = 0.017453292519943295 @@ -82,16 +121,17 @@ def format_dist(distance, unit): def format_time(seconds): # Return a string displaying the time given as seconds or minutes - if seconds <= 0.0: - return '{:.2f} seconds'.format(seconds) - elif seconds <= 1.0: - return '{:.2f} second'.format(seconds) - elif seconds < 60: - return '{:.2f} seconds'.format(seconds) - elif seconds > 60 and seconds < 3600: - minutes = seconds / 60 - return '{:.2f} minutes'.format(minutes) - return '{:.2f} seconds'.format(seconds) + num, duration = 0, long(round(seconds)) + runtime = [] + for period, unit in TIME_PERIODS[::-1]: + num, duration = divmod(duration, period) + if num: + p = '{0}{1}'.format(unit, 's'*(num!=1)) + runtime.append('{0} {1}'.format(num, p)) + + runtime.append('{0} second{1}'.format(duration, 's'*(duration!=1))) + + return ', '.join(runtime) def i2f(int): @@ -108,3 +148,89 @@ def print_yellow(message): def print_red(message): print(u'\033[91m' + message.decode('utf-8') + '\033[0m') + + +def float_equal(f1, f2, epsilon=1e-8): + if f1 > f2: + return f1 - f2 < epsilon + if f2 > f1: + return f2 - f1 < epsilon + return True + + +# pseudo mercator projection +EARTH_RADIUS_MAJ = 6378137.0 +EARTH_RADIUS_MIN = 6356752.3142 +RATIO = (EARTH_RADIUS_MIN / EARTH_RADIUS_MAJ) +ECCENT = sqrt(1.0 - RATIO**2) +COM = 0.5 * ECCENT + + +def coord2merc(lat, lng): + return lng2x(lng), lat2y(lat) + + +def merc2coord(vec): + return y2lat(vec[1]), x2lng(vec[0]) + + +def y2lat(y): + ts = exp(-y / EARTH_RADIUS_MAJ) + phi = pi / 2.0 - 2 * atan(ts) + dphi = 1.0 + for i in range(15): + if abs(dphi) < 0.000000001: + break + con = ECCENT * sin(phi) + dphi = pi / 2.0 - 2 * atan (ts * pow((1.0 - con) / (1.0 + con), COM)) - phi + phi += dphi + return rad2deg(phi) + + +def lat2y(lat): + lat = min(89.5, max(lat, -89.5)) + phi = deg2rad(lat) + sinphi = sin(phi) + con = ECCENT * sinphi + con = pow((1.0 - con) / (1.0 + con), COM) + ts = tan(0.5 * (pi * 0.5 - phi)) / con + return 0 - EARTH_RADIUS_MAJ * log(ts) + + +def x2lng(x): + return rad2deg(x) / EARTH_RADIUS_MAJ + + +def lng2x(lng): + return EARTH_RADIUS_MAJ * deg2rad(lng); + + +def deg2rad(deg): + return deg * pi / 180.0 + + +def rad2deg(rad): + return rad * 180.0 / pi + + +def find_biggest_cluster(radius, points, order=None): + graph = nx.Graph() + for point in points: + if order is 'lure_info': + f = point['latitude'], point['longitude'], point['lure_info']['lure_expires_timestamp_ms'] + else: + f = point['latitude'], point['longitude'], 0 + graph.add_node(f) + for node in graph.nodes(): + if node != f and distance(f[0], f[1], node[0], node[1]) <= radius*2: + graph.add_edge(f, node) + cliques = list(find_cliques(graph)) + if len(cliques) > 0: + max_clique = max(list(find_cliques(graph)), key=lambda l: (len(l), sum(x[2] for x in l))) + merc_clique = [coord2merc(x[0], x[1]) for x in max_clique] + clique_x, clique_y = zip(*merc_clique) + best_point = np.mean(clique_x), np.mean(clique_y) + best_coord = merc2coord(best_point) + return {'latitude': best_coord[0], 'longitude': best_coord[1], 'num_points': len(max_clique)} + else: + return None diff --git a/pokemongo_bot/constants.py b/pokemongo_bot/constants.py new file mode 100644 index 0000000000..b52aae45fb --- /dev/null +++ b/pokemongo_bot/constants.py @@ -0,0 +1,2 @@ +class Constants(object): + MAX_DISTANCE_FORT_IS_REACHABLE = 40 # meters diff --git a/pokemongo_bot/event_handlers/__init__.py b/pokemongo_bot/event_handlers/__init__.py new file mode 100644 index 0000000000..f0933a0e68 --- /dev/null +++ b/pokemongo_bot/event_handlers/__init__.py @@ -0,0 +1,2 @@ +from logging_handler import LoggingHandler +from socketio_handler import SocketIoHandler diff --git a/pokemongo_bot/event_handlers/logging_handler.py b/pokemongo_bot/event_handlers/logging_handler.py new file mode 100644 index 0000000000..7ad5720f6a --- /dev/null +++ b/pokemongo_bot/event_handlers/logging_handler.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +import logging + +from pokemongo_bot.event_manager import EventHandler + + +class LoggingHandler(EventHandler): + + def handle_event(self, event, sender, level, formatted_msg, data): + logger = logging.getLogger(type(sender).__name__) + if formatted_msg: + message = "[{}] {}".format(event, formatted_msg) + else: + message = '{}: {}'.format(event, str(data)) + getattr(logger, level)(message) diff --git a/pokemongo_bot/event_handlers/socketio_handler.py b/pokemongo_bot/event_handlers/socketio_handler.py new file mode 100644 index 0000000000..05b095313a --- /dev/null +++ b/pokemongo_bot/event_handlers/socketio_handler.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +from socketIO_client import SocketIO + +from pokemongo_bot.event_manager import EventHandler + + +class SocketIoHandler(EventHandler): + + + def __init__(self, bot, url): + self.bot = bot + self.host, port_str = url.split(':') + self.port = int(port_str) + self.sio = SocketIO(self.host, self.port) + + def handle_event(self, event, sender, level, msg, data): + if msg: + data['msg'] = msg + + self.sio.emit( + 'bot:broadcast', + { + 'event': event, + 'account': self.bot.config.username, + 'data': data + } + ) diff --git a/pokemongo_bot/event_manager.py b/pokemongo_bot/event_manager.py new file mode 100644 index 0000000000..3773ec8a9e --- /dev/null +++ b/pokemongo_bot/event_manager.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +class EventNotRegisteredException(Exception): + pass + + +class EventMalformedException(Exception): + pass + + +class EventHandler(object): + + def __init__(self): + pass + + def handle_event(self, event, kwargs): + raise NotImplementedError("Please implement") + + +class EventManager(object): + + def __init__(self, *handlers): + self._registered_events = dict() + self._handlers = handlers or [] + + def event_report(self): + for event, parameters in self._registered_events.iteritems(): + print '-'*80 + print 'Event: {}'.format(event) + if parameters: + print 'Parameters:' + for parameter in parameters: + print '* {}'.format(parameter) + + def add_handler(self, event_handler): + self._handlers.append(event_handler) + + def register_event(self, name, parameters=[]): + self._registered_events[name] = parameters + + def emit(self, event, sender=None, level='info', formatted='', data={}): + if not sender: + raise ArgumentError('Event needs a sender!') + + levels = ['info', 'warning', 'error', 'critical', 'debug'] + if not level in levels: + raise ArgumentError('Event level needs to be in: {}'.format(levels)) + + if event not in self._registered_events: + raise EventNotRegisteredException("Event %s not registered..." % event) + + # verify params match event + parameters = self._registered_events[event] + if parameters: + for k, v in data.iteritems(): + if k not in parameters: + raise EventMalformedException("Event %s does not require parameter %s" % (event, k)) + + formatted_msg = formatted.format(**data) + + # send off to the handlers + for handler in self._handlers: + handler.handle_event(event, sender, level, formatted_msg, data) diff --git a/pokemongo_bot/health_record/__init__.py b/pokemongo_bot/health_record/__init__.py new file mode 100644 index 0000000000..a40a959a1c --- /dev/null +++ b/pokemongo_bot/health_record/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from bot_event import BotEvent diff --git a/pokemongo_bot/health_record/bot_event.py b/pokemongo_bot/health_record/bot_event.py new file mode 100644 index 0000000000..986b5f3c70 --- /dev/null +++ b/pokemongo_bot/health_record/bot_event.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +from time import sleep + +import logging +from raven import Client +import raven +import os +import uuid +import requests + +class BotEvent(object): + def __init__(self, config): + self.config = config + self.logger = logging.getLogger(__name__) + # UniversalAnalytics can be reviewed here: + # https://github.com/analytics-pros/universal-analytics-python + if self.config.health_record: + self.logger.info('Health check is enabled. For more logrmation:') + self.logger.info('https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev#analytics') + self.client = Client( + dsn='https://8abac56480f34b998813d831de262514:196ae1d8dced41099f8253ea2c8fe8e6@app.getsentry.com/90254', + name='PokemonGof-Bot', + processors = ( + 'raven.processors.SanitizePasswordsProcessor', + 'raven.processors.RemoveStackLocalsProcessor' + ), + install_logging_hook = False, + hook_libraries = (), + enable_breadcrumbs = False, + logging = False, + context = {} + ) + + def capture_error(self): + if self.config.health_record: + self.client.captureException() + + def login_success(self): + if self.config.health_record: + track_url('/loggedin') + + def login_failed(self): + if self.config.health_record: + track_url('/login') + + def login_retry(self): + if self.config.health_record: + track_url('/relogin') + + def logout(self): + if self.config.health_record: + track_url('/logout') + + +def track_url(path): + data = { + 'v': '1', + 'tid': 'UA-81469507-1', + 'aip': '1', # Anonymize IPs + 'cid': uuid.uuid4(), + 't': 'pageview', + 'dp': path + } + + response = requests.post( + 'http://www.google-analytics.com/collect', data=data) + + response.raise_for_status() diff --git a/pokemongo_bot/human_behaviour.py b/pokemongo_bot/human_behaviour.py index 6d4c434f92..2a8d2d5e9f 100644 --- a/pokemongo_bot/human_behaviour.py +++ b/pokemongo_bot/human_behaviour.py @@ -1,17 +1,46 @@ # -*- coding: utf-8 -*- import time -from math import ceil -from random import random, randint +from random import random, uniform def sleep(seconds, delta=0.3): - jitter = ceil(delta * seconds) - sleep_time = randint(int(seconds - jitter), int(seconds + jitter)) - time.sleep(sleep_time) + time.sleep(jitter(seconds,delta)) + + +def jitter(value, delta=0.3): + jitter = delta * value + return uniform(value-jitter, value+jitter) + + +def action_delay(low, high): + # Waits for random number of seconds between low & high numbers + longNum = uniform(low, high) + shortNum = float("{0:.2f}".format(longNum)) + time.sleep(shortNum) def random_lat_long_delta(): # Return random value from [-.000025, .000025]. Since 364,000 feet is equivalent to one degree of latitude, this # should be 364,000 * .000025 = 9.1. So it returns between [-9.1, 9.1] return ((random() * 0.00001) - 0.000005) * 5 + + +# Humanized `normalized_reticle_size` parameter for `catch_pokemon` API. +# 1.0 => normal, 1.950 => excellent +def normalized_reticle_size(factor): + minimum = 1.0 + maximum = 1.950 + return uniform( + minimum + (maximum - minimum) * factor, + maximum) + + +# Humanized `spin_modifier` parameter for `catch_pokemon` API. +# 0.0 => normal ball, 1.0 => super spin curve ball +def spin_modifier(factor): + minimum = 0.0 + maximum = 1.0 + return uniform( + minimum + (maximum - minimum) * factor, + maximum) diff --git a/pokemongo_bot/lcd.py b/pokemongo_bot/lcd.py index c4f4da4b40..9a4eb87e6f 100644 --- a/pokemongo_bot/lcd.py +++ b/pokemongo_bot/lcd.py @@ -9,8 +9,6 @@ # By DenisFromHR (Denis Pleic) # 2015-02-10, ver 0.1 """ -# -# import os from itertools import islice from time import * @@ -52,13 +50,13 @@ def read_data(self, cmd): def read_block_data(self, cmd): return self.bus.read_block_data(self.addr, cmd) + # LCD Address -#ADDRESS = 0x27 +# ADDRESS = 0x27 LCD_WIDTH = 20 LCD_HEIGHT = 2 -LCD_CHARS = [0x40, 0x48, 0x50, 0x58, 0x60, 0x68, 0x70, - 0x78] #Address position for custom chars -#Use char generator here: https://omerk.github.io/lcdchargen/ or http://www.quinapalus.com/hd44780udg.html +LCD_CHARS = [0x40, 0x48, 0x50, 0x58, 0x60, 0x68, 0x70, 0x78] # Address position for custom chars +# Use char generator here: https://omerk.github.io/lcdchargen/ or http://www.quinapalus.com/hd44780udg.html # commands LCD_CLEARDISPLAY = 0x01 LCD_RETURNHOME = 0x02 @@ -108,7 +106,7 @@ def read_block_data(self, cmd): class lcd: # initializes objects and lcd - #def __init__(self, adress): + # def __init__(self, adress): def set_addr(self, adress): self.lcd_device = i2c_device(adress) diff --git a/pokemongo_bot/logger.py b/pokemongo_bot/logger.py index 151e578a0f..ebb59b7599 100644 --- a/pokemongo_bot/logger.py +++ b/pokemongo_bot/logger.py @@ -1,22 +1,20 @@ -import time -try: - import lcd - lcd = lcd.lcd() - # Change this to your i2c address - lcd.set_addr(0x23) -except: - lcd = False +import warnings +import logging -def log(string, color = 'white'): - colorHex = { - 'green': '92m', - 'yellow': '93m', - 'red': '91m' - } - if color not in colorHex: - print('[' + time.strftime("%Y-%m-%d %H:%M:%S") + '] '+ string) - else: - print(u'\033['+ colorHex[color] + '[' + time.strftime("%Y-%m-%d %H:%M:%S") + '] ' + string.decode('utf-8') + '\033[0m') - if lcd: - if(string): - lcd.message(string) + +def log(msg, color=None): + warnings.simplefilter('always', DeprecationWarning) + message = ( + "Using logger.log is deprecated and will be removed soon. " + "We recommend that you try to log as little as possible " + "and use the event system to send important messages " + "(they become logs and websocket messages) automatically). " + "If you don't think your message should go to the websocket " + "server but it's really necessary, use the self.logger variable " + "inside any class inheriting from BaseTask to log." + + ) + + logger = logging.getLogger('generic') + logger.info(msg) + warnings.warn(message, DeprecationWarning) diff --git a/pokemongo_bot/metrics.py b/pokemongo_bot/metrics.py new file mode 100644 index 0000000000..0ffeb39a6c --- /dev/null +++ b/pokemongo_bot/metrics.py @@ -0,0 +1,113 @@ +import time +from datetime import timedelta + + +class Metrics(object): + + def __init__(self, bot): + self.bot = bot + self.start_time = time.time() + self.dust = {'start': None, 'latest': None} + self.xp = {'start': None, 'latest': None} + self.distance = {'start': None, 'latest': None} + self.encounters = {'start': None, 'latest': None} + self.throws = {'start': None, 'latest': None} + self.captures = {'start': None, 'latest': None} + self.visits = {'start': None, 'latest': None} + self.unique_mons = {'start': None, 'latest': None} + self.evolutions = {'start': None, 'latest': None} + + self.releases = 0 + self.highest_cp = {'cp': 0, 'desc': ''} + self.most_perfect = {'potential': 0, 'desc': ''} + + def runtime(self): + return timedelta(seconds=round(time.time() - self.start_time)) + + def xp_earned(self): + return self.xp['latest'] - self.xp['start'] + + def xp_per_hour(self): + return self.xp_earned()/(time.time() - self.start_time)*3600 + + def distance_travelled(self): + return self.distance['latest'] - self.distance['start'] + + def num_encounters(self): + return self.encounters['latest'] - self.encounters['start'] + + def num_throws(self): + return self.throws['latest'] - self.throws['start'] + + def num_captures(self): + return self.captures['latest'] - self.captures['start'] + + def num_visits(self): + return self.visits['latest'] - self.visits['start'] + + def num_new_mons(self): + return self.unique_mons['latest'] - self.unique_mons['start'] + + def num_evolutions(self): + return self.evolutions['latest'] - self.evolutions['start'] + + def earned_dust(self): + return self.dust['latest'] - self.dust['start'] + + def captured_pokemon(self, name, cp, iv_display, potential): + if cp > self.highest_cp['cp']: + self.highest_cp = \ + {'cp': cp, 'desc': '{} [CP: {}] [IV: {}] Potential: {} ' + .format(name, cp, iv_display, potential)} + + if potential > self.most_perfect['potential']: + self.most_perfect = \ + {'potential': potential, 'desc': '{} [CP: {}] [IV: {}] Potential: {} ' + .format(name, cp, iv_display, potential)} + return + + def released_pokemon(self, count=1): + self.releases += count + + def capture_stats(self): + request = self.bot.api.create_request() + request.get_inventory() + request.get_player() + response_dict = request.call() + try: + self.dust['latest'] = response_dict['responses']['GET_PLAYER']['player_data']['currencies'][1]['amount'] + if self.dust['start'] is None: self.dust['start'] = self.dust['latest'] + for item in response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']: + if 'inventory_item_data' in item: + if 'player_stats' in item['inventory_item_data']: + playerdata = item['inventory_item_data']['player_stats'] + + self.xp['latest'] = playerdata.get('experience', 0) + if self.xp['start'] is None: self.xp['start'] = self.xp['latest'] + + self.visits['latest'] = playerdata.get('poke_stop_visits', 0) + if self.visits['start'] is None: self.visits['start'] = self.visits['latest'] + + self.captures['latest'] = playerdata.get('pokemons_captured', 0) + if self.captures['start'] is None: self.captures['start'] = self.captures['latest'] + + self.distance['latest'] = playerdata.get('km_walked', 0) + if self.distance['start'] is None: self.distance['start'] = self.distance['latest'] + + self.encounters['latest'] = playerdata.get('pokemons_encountered', 0) + if self.encounters['start'] is None: self.encounters['start'] = self.encounters['latest'] + + self.throws['latest'] = playerdata.get('pokeballs_thrown', 0) + if self.throws['start'] is None: self.throws['start'] = self.throws['latest'] + + self.unique_mons['latest'] = playerdata.get('unique_pokedex_entries', 0) + if self.unique_mons['start'] is None: self.unique_mons['start'] = self.unique_mons['latest'] + + self.visits['latest'] = playerdata.get('poke_stop_visits', 0) + if self.visits['start'] is None: self.visits['start'] = self.visits['latest'] + + self.evolutions['latest'] = playerdata.get('evolutions', 0) + if self.evolutions['start'] is None: self.evolutions['start'] = self.evolutions['latest'] + except KeyError: + # Nothing we can do if there's no player info. + return diff --git a/pokemongo_bot/polyline_stepper.py b/pokemongo_bot/polyline_stepper.py deleted file mode 100644 index 32d07fc1c9..0000000000 --- a/pokemongo_bot/polyline_stepper.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- -from polyline_walker import PolylineWalker -from stepper import Stepper -from human_behaviour import sleep, random_lat_long_delta - - -class PolylineStepper(Stepper): - - def _walk_to(self, speed, lat, lng, alt): - origin = ','.join([str(self.api._position_lat), str(self.api._position_lng)]) - destination = ','.join([str(lat), str(lng)]) - polyline_walker = PolylineWalker(origin, destination, self.speed) - proposed_origin = polyline_walker.points[0] - proposed_destination = polyline_walker.points[-1] - proposed_lat = proposed_origin[0] - proposed_lng = proposed_origin[1] - if proposed_lat != lat and proposed_lng != lng: - self._old_walk_to(speed, proposed_lat, proposed_lng, alt) - while proposed_destination != polyline_walker.get_pos()[0]: - cLat, cLng = polyline_walker.get_pos()[0] - self.api.set_position(cLat, cLng, alt) - self.bot.heartbeat() - self._work_at_position(i2f(self.api._position_lat), i2f(self.api._position_lng), alt, False) - sleep(1) # sleep one second plus a random delta - if proposed_lat != self.api._position_lat and proposed_lng != self.api._position_lng: - self._old_walk_to(speed, lat, lng, alt) - - def _old_walk_to(self, speed, lat, lng, alt): - dist = distance( - i2f(self.api._position_lat), i2f(self.api._position_lng), lat, lng) - steps = (dist + 0.0) / (speed + 0.0) # may be rational number - intSteps = int(steps) - residuum = steps - intSteps - logger.log('[#] Walking from ' + str((i2f(self.api._position_lat), i2f( - self.api._position_lng))) + " to " + str(str((lat, lng))) + - " for approx. " + str(format_time(ceil(steps)))) - if steps != 0: - dLat = (lat - i2f(self.api._position_lat)) / steps - dLng = (lng - i2f(self.api._position_lng)) / steps - - for i in range(intSteps): - cLat = i2f(self.api._position_lat) + \ - dLat + random_lat_long_delta() - cLng = i2f(self.api._position_lng) + \ - dLng + random_lat_long_delta() - self.api.set_position(cLat, cLng, alt) - self.bot.heartbeat() - sleep(1) # sleep one second plus a random delta - self._work_at_position( - i2f(self.api._position_lat), i2f(self.api._position_lng), - alt, False) - - self.api.set_position(lat, lng, alt) - self.bot.heartbeat() - logger.log("[#] Finished walking") - diff --git a/pokemongo_bot/socketio_server/__init__.py b/pokemongo_bot/socketio_server/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pokemongo_bot/socketio_server/app.py b/pokemongo_bot/socketio_server/app.py new file mode 100644 index 0000000000..09c237f910 --- /dev/null +++ b/pokemongo_bot/socketio_server/app.py @@ -0,0 +1,32 @@ +import logging + +import socketio +from flask import Flask + + +sio = socketio.Server(async_mode='eventlet', logging=logging.NullHandler) +app = Flask(__name__) + +# client asks for data +@sio.on('remote:send_request') +def remote_control(sid, command): + if not 'account' in command: + return False + bot_name = command.pop('account') + event = 'bot:process_request:{}'.format(bot_name) + sio.emit(event, data=command) + +# sending bot response to client +@sio.on('bot:send_reply') +def request_reply(sid, response): + event = response.pop('command') + account = response.pop('account') + event = "{}:{}".format(event, account) + sio.emit(event, response) + +@sio.on('bot:broadcast') +def bot_broadcast(sid, env): + event = env.pop('event') + account = env.pop('account') + event_name = "{}:{}".format(event, account) + sio.emit(event_name, data=env['data']) diff --git a/pokemongo_bot/socketio_server/runner.py b/pokemongo_bot/socketio_server/runner.py new file mode 100644 index 0000000000..1a27fd8ff7 --- /dev/null +++ b/pokemongo_bot/socketio_server/runner.py @@ -0,0 +1,34 @@ +import threading + +import eventlet +import socketio +from eventlet import patcher, wsgi + +from app import app, sio + +patcher.monkey_patch(all=True) + + +class SocketIoRunner(object): + def __init__(self, url): + self.host, port_str = url.split(':') + self.port = int(port_str) + self.server = None + + # create the thread object + self.thread = threading.Thread(target=self._start_listening_blocking) + + # wrap Flask application with socketio's middleware + self.app = socketio.Middleware(sio, app) + + def start_listening_async(self): + wsgi.is_accepting = True + self.thread.start() + + def stop_listening(self): + wsgi.is_accepting = False + + def _start_listening_blocking(self): + # deploy as an eventlet WSGI server + listener = eventlet.listen((self.host, self.port)) + self.server = wsgi.server(listener, self.app, log_output=False, debug=False) diff --git a/pokemongo_bot/step_walker.py b/pokemongo_bot/step_walker.py new file mode 100644 index 0000000000..263699095d --- /dev/null +++ b/pokemongo_bot/step_walker.py @@ -0,0 +1,65 @@ +from math import sqrt + +from cell_workers.utils import distance +from human_behaviour import random_lat_long_delta, sleep + + +class StepWalker(object): + + def __init__(self, bot, speed, dest_lat, dest_lng): + self.bot = bot + self.api = bot.api + + self.initLat, self.initLng = self.bot.position[0:2] + + self.dist = distance( + self.initLat, + self.initLng, + dest_lat, + dest_lng + ) + + self.speed = speed + + self.destLat = dest_lat + self.destLng = dest_lng + self.totalDist = max(1, self.dist) + + self.steps = (self.dist + 0.0) / (speed + 0.0) + + if self.dist < speed or int(self.steps) <= 1: + self.dLat = 0 + self.dLng = 0 + self.magnitude = 0 + else: + self.dLat = (dest_lat - self.initLat) / int(self.steps) + self.dLng = (dest_lng - self.initLng) / int(self.steps) + self.magnitude = self._pythagorean(self.dLat, self.dLng) + + def step(self): + if (self.dLat == 0 and self.dLng == 0) or self.dist < self.speed: + self.api.set_position(self.destLat, self.destLng, 0) + return True + + totalDLat = (self.destLat - self.initLat) + totalDLng = (self.destLng - self.initLng) + magnitude = self._pythagorean(totalDLat, totalDLng) + unitLat = totalDLat / magnitude + unitLng = totalDLng / magnitude + + scaledDLat = unitLat * self.magnitude + scaledDLng = unitLng * self.magnitude + + cLat = self.initLat + scaledDLat + random_lat_long_delta() + cLng = self.initLng + scaledDLng + random_lat_long_delta() + + self.api.set_position(cLat, cLng, 0) + self.bot.heartbeat() + + sleep(1) # sleep one second plus a random delta + # self._work_at_position( + # self.initLat, self.initLng, + # alt, False) + + def _pythagorean(self, lat, lng): + return sqrt((lat ** 2) + (lng ** 2)) diff --git a/pokemongo_bot/stepper.py b/pokemongo_bot/stepper.py deleted file mode 100644 index fda5e63fb8..0000000000 --- a/pokemongo_bot/stepper.py +++ /dev/null @@ -1,158 +0,0 @@ -# -*- coding: utf-8 -*- - -import os -import json -import time -import pprint - -from math import ceil -from s2sphere import CellId, LatLng -from google.protobuf.internal import encoder - -from human_behaviour import sleep, random_lat_long_delta -from cell_workers.utils import distance, i2f, format_time - -from pgoapi.utilities import f2i, h2f -import logger - - -class Stepper(object): - def __init__(self, bot): - self.bot = bot - self.api = bot.api - self.config = bot.config - - self.pos = 1 - self.x = 0 - self.y = 0 - self.dx = 0 - self.dy = -1 - self.steplimit = self.config.max_steps - self.steplimit2 = self.steplimit**2 - self.origin_lat = self.bot.position[0] - self.origin_lon = self.bot.position[1] - - def take_step(self): - position = (self.origin_lat, self.origin_lon, 0.0) - - self.api.set_position(*position) - for step in range(self.steplimit2): - # starting at 0 index - logger.log('[#] Scanning area for objects ({} / {})'.format( - (step + 1), self.steplimit**2)) - if self.config.debug: - logger.log( - 'steplimit: {} x: {} y: {} pos: {} dx: {} dy {}'.format( - self.steplimit2, self.x, self.y, self.pos, self.dx, - self.dy)) - # Scan location math - if -self.steplimit2 / 2 < self.x <= self.steplimit2 / 2 and -self.steplimit2 / 2 < self.y <= self.steplimit2 / 2: - position = (self.x * 0.0025 + self.origin_lat, - self.y * 0.0025 + self.origin_lon, 0) - if self.config.walk > 0: - self._walk_to(self.config.walk, *position) - else: - self.api.set_position(*position) - print('[#] {}'.format(position)) - if self.x == self.y or self.x < 0 and self.x == -self.y or self.x > 0 and self.x == 1 - self.y: - (self.dx, self.dy) = (-self.dy, self.dx) - - (self.x, self.y) = (self.x + self.dx, self.y + self.dy) - - self._work_at_position(position[0], position[1], position[2], True) - sleep(10) - - def _walk_to(self, speed, lat, lng, alt): - dist = distance( - i2f(self.api._position_lat), i2f(self.api._position_lng), lat, lng) - steps = (dist + 0.0) / (speed + 0.0) # may be rational number - intSteps = int(steps) - residuum = steps - intSteps - logger.log('[#] Walking from ' + str((i2f(self.api._position_lat), i2f( - self.api._position_lng))) + " to " + str(str((lat, lng))) + - " for approx. " + str(format_time(ceil(steps)))) - if steps != 0: - dLat = (lat - i2f(self.api._position_lat)) / steps - dLng = (lng - i2f(self.api._position_lng)) / steps - - for i in range(intSteps): - cLat = i2f(self.api._position_lat) + \ - dLat + random_lat_long_delta() - cLng = i2f(self.api._position_lng) + \ - dLng + random_lat_long_delta() - self.api.set_position(cLat, cLng, alt) - self.bot.heartbeat() - sleep(1) # sleep one second plus a random delta - self._work_at_position( - i2f(self.api._position_lat), i2f(self.api._position_lng), - alt, False) - - self.api.set_position(lat, lng, alt) - self.bot.heartbeat() - logger.log("[#] Finished walking") - - def _work_at_position(self, lat, lng, alt, pokemon_only=False): - cellid = self._get_cellid(lat, lng) - timestamp = [0, ] * len(cellid) - self.api.get_map_objects(latitude=f2i(lat), - longitude=f2i(lng), - since_timestamp_ms=timestamp, - cell_id=cellid) - - response_dict = self.api.call() - # pprint.pprint(response_dict) - # Passing Variables through a file - if response_dict and 'responses' in response_dict: - if 'GET_MAP_OBJECTS' in response_dict['responses']: - if 'map_cells' in response_dict['responses'][ - 'GET_MAP_OBJECTS']: - user_web_location = 'web/location-%s.json' % (self.config.username) - if os.path.isfile(user_web_location): - with open(user_web_location, 'w') as outfile: - json.dump( - {'lat': lat, - 'lng': lng, - 'cells': response_dict[ - 'responses']['GET_MAP_OBJECTS']['map_cells']}, - outfile) - - user_data_lastlocation = 'data/last-location-%s.json' % (self.config.username) - if os.path.isfile(user_data_lastlocation): - with open(user_data_lastlocation, 'w') as outfile: - outfile.truncate() - json.dump({'lat': lat, 'lng': lng}, outfile) - - if response_dict and 'responses' in response_dict: - if 'GET_MAP_OBJECTS' in response_dict['responses']: - if 'status' in response_dict['responses']['GET_MAP_OBJECTS']: - if response_dict['responses']['GET_MAP_OBJECTS'][ - 'status'] is 1: - map_cells = response_dict['responses'][ - 'GET_MAP_OBJECTS']['map_cells'] - position = (lat, lng, alt) - # Sort all by distance from current pos- eventually this should build graph & A* it - # print(map_cells) - #print( s2sphere.from_token(x['s2_cell_id']) ) - map_cells.sort(key=lambda x: distance(lat, lng, x['forts'][0]['latitude'], x[ - 'forts'][0]['longitude']) if 'forts' in x and x['forts'] != [] else 1e6) - for cell in map_cells: - self.bot.work_on_cell(cell, position, pokemon_only) - - def _get_cellid(self, lat, long, radius=10): - origin = CellId.from_lat_lng(LatLng.from_degrees(lat, long)).parent(15) - walk = [origin.id()] - - # 10 before and 10 after - next = origin.next() - prev = origin.prev() - for i in range(radius): - walk.append(prev.id()) - walk.append(next.id()) - next = next.next() - prev = prev.prev() - return sorted(walk) - - def _encode(self, cellid): - output = [] - encoder._VarintEncoder()(output.append, cellid) - return ''.join(output) diff --git a/pokemongo_bot/test/__init__.py b/pokemongo_bot/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pokemongo_bot/test/follow_cluster_test.py b/pokemongo_bot/test/follow_cluster_test.py new file mode 100644 index 0000000000..4820a7558a --- /dev/null +++ b/pokemongo_bot/test/follow_cluster_test.py @@ -0,0 +1,42 @@ +import unittest, pickle, os +from mock import patch +from pokemongo_bot.cell_workers.follow_cluster import FollowCluster + + +class FollowClusterTestCase(unittest.TestCase): + + @patch('pokemongo_bot.PokemonGoBot') + def testWorkAway(self, mock_pokemongo_bot): + forts_path = os.path.join(os.path.dirname(__file__), 'resources', 'example_forts.pickle') + with open(forts_path, 'rb') as forts: + ex_forts = pickle.load(forts) + config = {'radius': 50, 'lured': False} + mock_pokemongo_bot.position = (37.396787, -5.994587) + mock_pokemongo_bot.config.walk = 4.16 + mock_pokemongo_bot.get_forts.return_value = ex_forts + follow_cluster = FollowCluster(mock_pokemongo_bot, config) + + expected = (37.397183750142624, -5.9932912500000013) + result = follow_cluster.work() + self.assertAlmostEqual(expected[0], result[0], delta=0.000000000010000) + self.assertAlmostEqual(expected[1], result[1], delta=0.000000000010000) + assert follow_cluster.is_at_destination == False + assert follow_cluster.announced == False + + @patch('pokemongo_bot.PokemonGoBot') + def testWorkArrived(self, mock_pokemongo_bot): + forts_path = os.path.join(os.path.dirname(__file__), 'resources', 'example_forts.pickle') + with open(forts_path, 'rb') as forts: + ex_forts = pickle.load(forts) + config = {'radius': 50, 'lured': False} + mock_pokemongo_bot.position = (37.39718375014263, -5.9932912500000013) + mock_pokemongo_bot.config.walk = 4.16 + mock_pokemongo_bot.get_forts.return_value = ex_forts + follow_cluster = FollowCluster(mock_pokemongo_bot, config) + + expected = (37.397183750142624, -5.9932912500000013) + result = follow_cluster.work() + self.assertAlmostEqual(expected[0], result[0], delta=0.000000000010000) + self.assertAlmostEqual(expected[1], result[1], delta=0.000000000010000) + assert follow_cluster.is_at_destination == True + assert follow_cluster.announced == False diff --git a/pokemongo_bot/test/resources/example_forts.pickle b/pokemongo_bot/test/resources/example_forts.pickle new file mode 100644 index 0000000000..83e516a845 --- /dev/null +++ b/pokemongo_bot/test/resources/example_forts.pickle @@ -0,0 +1,1415 @@ +(lp0 +(dp1 +S'last_modified_timestamp_ms' +p2 +L1469921475597L +sS'enabled' +p3 +I01 +sS'longitude' +p4 +F-5.993626 +sS'latitude' +p5 +F37.398013 +sS'type' +p6 +I1 +sS'id' +p7 +V251b8fed23fc423a830f90f2ba50c305.16 +p8 +sa(dp9 +g2 +L1469898328246L +sg3 +I01 +sg4 +F-5.9938 +sg5 +F37.397335 +sg6 +I1 +sg7 +V29e754fd9e364b1badcfbea89716f596.16 +p10 +sa(dp11 +g2 +L1469897243389L +sg3 +I01 +sg4 +F-5.993398 +sS'cooldown_complete_timestamp_ms' +p12 +L1469960466296L +sg5 +F37.397007 +sg6 +I1 +sg7 +V31cb76c028be429abf25c28a27fa5663.16 +p13 +sa(dp14 +g2 +L1469959534933L +sg3 +I01 +sg4 +F-5.991949 +sg5 +F37.397225 +sg6 +I1 +sg7 +V3459c81701da414e859cb0df5cde072f.16 +p15 +sa(dp16 +g2 +L1469374995437L +sg3 +I01 +sg4 +F-5.9924 +sg5 +F37.398369 +sg6 +I1 +sg7 +V4335073e77784d6cbeec259b707aee24.16 +p17 +sa(dp18 +g2 +L1469391332349L +sg3 +I01 +sg4 +F-5.991212 +sg5 +F37.396881 +sg6 +I1 +sg7 +V56c3ae94b6db4beb8d2a514f89b8d009.16 +p19 +sa(dp20 +g2 +L1469659319870L +sg3 +I01 +sg4 +F-5.992902 +sg12 +L1469960449891L +sg5 +F37.397015 +sg6 +I1 +sg7 +Va8ddb011a6bf4e528da833d7f8f1a573.16 +p21 +sa(dp22 +g2 +L1469778820610L +sg3 +I01 +sg4 +F-5.991572 +sg5 +F37.397664 +sg6 +I1 +sg7 +Vad48c742304149c691410af4104d6baa.16 +p23 +sa(dp24 +g2 +L1469643608920L +sg3 +I01 +sg4 +F-5.992462 +sg5 +F37.397701 +sg6 +I1 +sg7 +Ve79dbe80660549a0b3bd0dac15a99b4d.16 +p25 +sa(dp26 +g2 +L1469653812627L +sg3 +I01 +sg4 +F-5.993065 +sg12 +L1469960392023L +sg5 +F37.397378 +sg6 +I1 +sg7 +Vf5098ca9946c4d31aff4aab3875bf0f1.16 +p27 +sa(dp28 +g2 +L1469816066943L +sg3 +I01 +sg4 +F-5.99184 +sg5 +F37.395478 +sg6 +I1 +sg7 +V3918a8ebf8144446b27859e17362d238.16 +p29 +sa(dp30 +g2 +L1469733518512L +sg3 +I01 +sg4 +F-5.991272 +sg5 +F37.396256 +sg6 +I1 +sg7 +V3f51bfd8dc4b4984956374289f68059a.16 +p31 +sa(dp32 +g2 +L1469862148216L +sg3 +I01 +sg4 +F-5.991457 +sg5 +F37.39461 +sg6 +I1 +sg7 +V80c964060e494071ab456031ecc38718.16 +p33 +sa(dp34 +g2 +L1469833795206L +sg3 +I01 +sg4 +F-5.99319 +sg5 +F37.396199 +sg6 +I1 +sg7 +Va14d099f61d346cf91ca1bb18d6eeb6d.16 +p35 +sa(dp36 +g2 +L1469949486673L +sg3 +I01 +sg4 +F-5.992714 +sg5 +F37.39437 +sg6 +I1 +sg7 +Vda803c899d394a4aadd187d4572db120.16 +p37 +sa(dp38 +g2 +L1469963502716L +sg3 +I01 +sg4 +F-5.992074 +sg5 +F37.395383 +sg6 +I1 +sg7 +Vde9799520ce9438686a3a8edf1f82c6a.16 +p39 +sa(dp40 +g2 +L1469724649385L +sg3 +I01 +sg4 +F-5.99095 +sg5 +F37.398202 +sg6 +I1 +sg7 +V00a553c0856d4751b7b03ca5d935ef32.16 +p41 +sa(dp42 +g2 +L1469752401817L +sg3 +I01 +sg4 +F-5.989081 +sg5 +F37.398749 +sg6 +I1 +sg7 +V511826b74c2749beaf6a1a53ac9a6d6a.16 +p43 +sa(dp44 +g2 +L1469908392925L +sg3 +I01 +sg4 +F-5.989965 +sg5 +F37.398617 +sg6 +I1 +sg7 +V844891f9ff4342ef8ec3c9409a20da65.16 +p45 +sa(dp46 +g2 +L1469671694157L +sg3 +I01 +sg4 +F-5.990034 +sg5 +F37.399266 +sg6 +I1 +sg7 +Vdd74f0db5da84c4e90ffa0c1a9d6b206.16 +p47 +sa(dp48 +g2 +L1469528338773L +sg3 +I01 +sg4 +F-5.99098 +sg5 +F37.39896 +sg6 +I1 +sg7 +Ve276734ef68c4841b34fb66aada7eae1.16 +p49 +sa(dp50 +g2 +L1469158302943L +sg3 +I01 +sg4 +F-5.995018 +sg5 +F37.398226 +sg6 +I1 +sg7 +V80f84c3030e149f5bce2c5c2ca6058ba.16 +p51 +sa(dp52 +g2 +L1469910731097L +sg3 +I01 +sg4 +F-5.994533 +sg5 +F37.397635 +sg6 +I1 +sg7 +V8275d7d6b4ea44cf9656d6755715ba36.16 +p53 +sa(dp54 +g2 +L1469701402941L +sg3 +I01 +sg4 +F-5.995466 +sg5 +F37.398241 +sg6 +I1 +sg7 +V82860741d9434db58930be61817d5098.16 +p55 +sa(dp56 +g2 +L1469829182652L +sg3 +I01 +sg4 +F-5.994129 +sg5 +F37.399101 +sg6 +I1 +sg7 +V918befd170f54623868e51ce6a5d0ea9.11 +p57 +sa(dp58 +g2 +L1469908391471L +sg3 +I01 +sg4 +F-5.996385 +sg5 +F37.39721 +sg6 +I1 +sg7 +Vbd40e14e67fd4256845cca680866c3bf.16 +p59 +sa(dp60 +g2 +L1469918221191L +sg3 +I01 +sg4 +F-5.9941 +sg5 +F37.396979 +sg6 +I1 +sg7 +Vf5d1bb17378844ef8f5f181fd465b788.12 +p61 +sa(dp62 +g2 +L1469914861870L +sg3 +I01 +sg4 +F-5.99357 +sg5 +F37.400234 +sg6 +I1 +sg7 +V18712b17886140648e15a168b28810c0.12 +p63 +sa(dp64 +g2 +L1469730155405L +sg3 +I01 +sg4 +F-5.992908 +sg5 +F37.401593 +sg6 +I1 +sg7 +V49ec2372b10a4bf9b0667827cc9c7739.16 +p65 +sa(dp66 +g2 +L1469919202842L +sg3 +I01 +sg4 +F-5.993618 +sg5 +F37.400731 +sg6 +I1 +sg7 +V50ca2dbaae734ef98f69551dcc13052c.16 +p67 +sa(dp68 +g2 +L1469952945445L +sg3 +I01 +sg4 +F-5.991999 +sg5 +F37.400441 +sg6 +I1 +sg7 +V8a6f6662b93f4816b0749c15dfdf4118.16 +p69 +sa(dp70 +g2 +L1469963417085L +sg3 +I01 +sg4 +F-5.991508 +sg5 +F37.400873 +sg6 +I1 +sg7 +V9ab899ef25d842fb86ecf644e6668606.16 +p71 +sa(dp72 +g2 +L1469910941776L +sg3 +I01 +sg4 +F-5.993709 +sg5 +F37.399425 +sg6 +I1 +sg7 +Va4f7a938eb0a43cb9b67b095ad20a163.16 +p73 +sa(dp74 +g2 +L1469793537459L +sg3 +I01 +sg4 +F-5.991646 +sg5 +F37.399457 +sg6 +I1 +sg7 +Vc73349b5097348d4be65278c53a3a277.16 +p75 +sa(dp76 +g2 +L1468968208521L +sg3 +I01 +sg4 +F-5.990076 +sg5 +F37.39991 +sg6 +I1 +sg7 +V0259d9df56be4bcb960c44a774f75069.16 +p77 +sa(dp78 +g2 +L1469524106708L +sg3 +I01 +sg4 +F-5.990137 +sg5 +F37.400513 +sg6 +I1 +sg7 +V136c13edc75049b0b0f5cecef79a8afd.16 +p79 +sa(dp80 +g2 +L1469837426493L +sg3 +I01 +sg4 +F-5.990839 +sg5 +F37.399287 +sg6 +I1 +sg7 +V1ef6ea9edb3a4f64bdb833e5e205b5a3.16 +p81 +sa(dp82 +g2 +L1469938617325L +sg3 +I01 +sg4 +F-5.989291 +sg5 +F37.399426 +sg6 +I1 +sg7 +V74f43f0db4034f48be0aae963c50e3aa.16 +p83 +sa(dp84 +g2 +L1469962338544L +sg3 +I01 +sg4 +F-5.990811 +sg5 +F37.400943 +sg6 +I1 +sg7 +Va2797ccc46ce44e194adb3a19c69bd0a.16 +p85 +sa(dp86 +g2 +L1469557381652L +sg3 +I01 +sg4 +F-5.988956 +sg5 +F37.401278 +sg6 +I1 +sg7 +Vae554300c012425b8dc58240c4c80559.16 +p87 +sa(dp88 +g2 +L1469624514841L +sg3 +I01 +sg4 +F-5.990919 +sg5 +F37.400392 +sg6 +I1 +sg7 +Vbee12e766b114d2584de4e5f165416d7.16 +p89 +sa(dp90 +g2 +L1469917046183L +sg3 +I01 +sg4 +F-5.989457 +sg5 +F37.400447 +sg6 +I1 +sg7 +Ve96312dfd198478c88e70b1feed9a9e6.16 +p91 +sa(dp92 +g2 +L1469918359052L +sg3 +I01 +sg4 +F-5.990548 +sg5 +F37.401798 +sg6 +I1 +sg7 +Vef75f531b24a420e8c6c3cbfec68f11b.16 +p93 +sa(dp94 +g2 +L1469274803405L +sg3 +I01 +sg4 +F-5.989557 +sg5 +F37.394804 +sg6 +I1 +sg7 +V53fbb24493d14b15ad134c11adb3b0d4.16 +p95 +sa(dp96 +g2 +L1469740978120L +sg3 +I01 +sg4 +F-5.990471 +sg5 +F37.394447 +sg6 +I1 +sg7 +V7004401fb17145ddb12a131a81a177dd.16 +p97 +sa(dp98 +g2 +L1469554523140L +sg3 +I01 +sg4 +F-5.990258 +sg5 +F37.394936 +sg6 +I1 +sg7 +Vee8401b906bc4c4e90ac21bfb765f08d.16 +p99 +sa(dp100 +g2 +L1469232383352L +sg3 +I01 +sg4 +F-5.990434 +sg5 +F37.396289 +sg6 +I1 +sg7 +Vf1b217803ff14b6f8d92f79a592ae899.16 +p101 +sa(dp102 +g2 +L1469635023323L +sg3 +I01 +sg4 +F-5.988083 +sg5 +F37.39839 +sg6 +I1 +sg7 +V3e84fd1c22c94c1f98c8ad77750212f9.16 +p103 +sa(dp104 +g2 +L1467338046145L +sg3 +I01 +sg4 +F-5.987945 +sg5 +F37.398004 +sg6 +I1 +sg7 +V5cf76c6ab11542e3b0b95ccefc15165c.16 +p105 +sa(dp106 +g2 +L1469922064608L +sg3 +I01 +sg4 +F-5.987965 +sg5 +F37.396849 +sg6 +I1 +sg7 +Vdd031ddba0fb4325952c02675319383e.16 +p107 +sa(dp108 +g2 +L1469529349540L +sg3 +I01 +sg4 +F-5.988186 +sg5 +F37.398902 +sg6 +I1 +sg7 +Ve82311a4b53441e193e261136997c0c9.16 +p109 +sa(dp110 +g2 +L1469823571443L +sg3 +I01 +sg4 +F-5.994268 +sg5 +F37.401455 +sg6 +I1 +sg7 +Vd99b00cbb06845cc8499be1729c5795f.16 +p111 +sa(dp112 +g2 +L1469733713485L +sg3 +I01 +sg4 +F-5.995301 +sg5 +F37.400897 +sg6 +I1 +sg7 +Vddf11d8e4c7145b189b92b2735d0024f.16 +p113 +sa(dp114 +g2 +L1469800536694L +sg3 +I01 +sg4 +F-5.987715 +sg5 +F37.396294 +sg6 +I1 +sg7 +V0c7ca6878b564955ae64ee67583dfd12.16 +p115 +sa(dp116 +g2 +L1469873160929L +sg3 +I01 +sg4 +F-5.986963 +sg5 +F37.395385 +sg6 +I1 +sg7 +V2492e99faa3f46f687fd116944f0a1fd.16 +p117 +sa(dp118 +g2 +L1469877329921L +sg3 +I01 +sg4 +F-5.986343 +sg5 +F37.394868 +sg6 +I1 +sg7 +V553b3576e4f54eef8db0b6d2d91530fa.16 +p119 +sa(dp120 +g2 +L1467338046145L +sg3 +I01 +sg4 +F-5.985946 +sg5 +F37.396228 +sg6 +I1 +sg7 +V6caaf31aa22745f4a755532c71dedb9b.16 +p121 +sa(dp122 +g2 +L1469915504786L +sg3 +I01 +sg4 +F-5.986965 +sg5 +F37.396379 +sg6 +I1 +sg7 +Vb04109647b874d0aaa3de9512f12a3be.16 +p123 +sa(dp124 +g2 +L1469708371732L +sg3 +I01 +sg4 +F-5.999056 +sg5 +F37.396844 +sg6 +I1 +sg7 +V677f13616dae49b192ddde32b36e9ac1.16 +p125 +sa(dp126 +g2 +L1469826712933L +sg3 +I01 +sg4 +F-5.998739 +sg5 +F37.398765 +sg6 +I1 +sg7 +V78a89cc750924facb795ae525db994fc.16 +p127 +sa(dp128 +g2 +L1469908360503L +sg3 +I01 +sg4 +F-5.996781 +sg5 +F37.397039 +sg6 +I1 +sg7 +V7b76693d36e94a81b5f34f223ff1aaf3.11 +p129 +sa(dp130 +g2 +L1469907235683L +sg3 +I01 +sg4 +F-5.996629 +sg5 +F37.39686 +sg6 +I1 +sg7 +V8c4e707902b8451e981b252fff361a6a.16 +p131 +sa(dp132 +g2 +L1469556189588L +sg3 +I01 +sg4 +F-5.996728 +sg5 +F37.398118 +sg6 +I1 +sg7 +Vbc034ed05c32411692092a00f63a57b6.16 +p133 +sa(dp134 +g2 +L1469283047906L +sg3 +I01 +sg4 +F-5.998928 +sg5 +F37.397511 +sg6 +I1 +sg7 +Ve42813ce3cd843128c78c3d899986efa.16 +p135 +sa(dp136 +g2 +L1469871321651L +sg3 +I01 +sg4 +F-5.990257 +sg5 +F37.392608 +sg6 +I1 +sg7 +V091a36bea52f4cd9966c3a731e4ea9ff.11 +p137 +sa(dp138 +g2 +L1469821135378L +sg3 +I01 +sg4 +F-5.988966 +sg5 +F37.392712 +sg6 +I1 +sg7 +V24f958ae33b641b3b671484cf05c37d2.16 +p139 +sa(dp140 +g2 +L1469870325686L +sg3 +I01 +sg4 +F-5.991149 +sg5 +F37.39207 +sg6 +I1 +sg7 +V70ba833513274dc8bc558f79479fe980.16 +p141 +sa(dp142 +g2 +L1469653517541L +sg3 +I01 +sg4 +F-5.991137 +sg5 +F37.392742 +sg6 +I1 +sg7 +V83b1276113e149a6877f5cf73115680f.16 +p143 +sa(dp144 +g2 +L1469824630387L +sg3 +I01 +sg4 +F-5.989751 +sg5 +F37.391884 +sg6 +I1 +sg7 +Vb3770ec05c9147eaab4fe62672f005bf.16 +p145 +sa(dp146 +g2 +L1469649071116L +sg3 +I01 +sg4 +F-5.990481 +sg5 +F37.391728 +sg6 +I1 +sg7 +Vfe6454150fd1422b8ad1fd5c8a3a7c40.11 +p147 +sa(dp148 +g2 +L1467338046145L +sg3 +I01 +sg4 +F-5.993713 +sg5 +F37.391965 +sg6 +I1 +sg7 +V139ac87bc96b415dbbe74c669eb3be72.16 +p149 +sa(dp150 +g2 +L1469827559172L +sg3 +I01 +sg4 +F-5.992554 +sg5 +F37.392737 +sg6 +I1 +sg7 +V177e45b2924e4311b4665a69457f71ae.16 +p151 +sa(dp152 +g2 +L1469881093724L +sg3 +I01 +sg4 +F-5.993334 +sg5 +F37.393651 +sg6 +I1 +sg7 +V1c530a0dfd084ff0954b83a8531f6622.16 +p153 +sa(dp154 +g2 +L1469832326396L +sg3 +I01 +sg4 +F-5.993711 +sg5 +F37.392736 +sg6 +I1 +sg7 +V2370a972f47b488a84b4c383b8a03b53.11 +p155 +sa(dp156 +g2 +L1467338046145L +sg3 +I01 +sg4 +F-5.99289 +sg5 +F37.392406 +sg6 +I1 +sg7 +V6d7a5bb65edf4b0caec439770b4b032d.16 +p157 +sa(dp158 +g2 +L1469918510112L +sg3 +I01 +sg4 +F-5.991879 +sg5 +F37.392437 +sg6 +I1 +sg7 +Vb7beef68d23c4dc7afc2bbbb5c8034d9.16 +p159 +sa(dp160 +g2 +L1467338046146L +sg3 +I01 +sg4 +F-5.997456 +sg5 +F37.401344 +sg6 +I1 +sg7 +V20c49edbf515435b930ba5ab174e6cef.16 +p161 +sa(dp162 +g2 +L1467338046146L +sg3 +I01 +sg4 +F-5.997457 +sg5 +F37.400579 +sg6 +I1 +sg7 +V4df60f277b2f4a279e00178f6943d91e.16 +p163 +sa(dp164 +g2 +L1469650281423L +sg3 +I01 +sg4 +F-5.996481 +sg5 +F37.400605 +sg6 +I1 +sg7 +V975f2575f377477cabf0b8f5b8ad8d24.16 +p165 +sa(dp166 +g2 +L1469664434366L +sg3 +I01 +sg4 +F-6.000522 +sg5 +F37.39915 +sg6 +I1 +sg7 +V9cb368ad5e5842a5807761bac0df2361.11 +p167 +sa(dp168 +g2 +L1469704243449L +sg3 +I01 +sg4 +F-6.0 +sg5 +F37.399723 +sg6 +I1 +sg7 +Va1fe11bcbb844b7fb14d77634c8eba0b.16 +p169 +sa(dp170 +g2 +L1469922802991L +sg3 +I01 +sg4 +F-5.986843 +sg5 +F37.393024 +sg6 +I1 +sg7 +V41255acb114042c4ba8621174167b7a3.16 +p171 +sa(dp172 +g2 +L1469733237934L +sg3 +I01 +sg4 +F-5.988198 +sg5 +F37.394002 +sg6 +I1 +sg7 +V7d03c69e92a2464fb1b0c2fbb1dab905.12 +p173 +sa(dp174 +g2 +L1469729857242L +sg3 +I01 +sg4 +F-5.986549 +sg5 +F37.39156 +sg6 +I1 +sg7 +Vb3c32ce075c04a2a8b113a681468c8f6.16 +p175 +sa(dp176 +g2 +L1469799670291L +sg3 +I01 +sg4 +F-5.98837 +sg5 +F37.393029 +sg6 +I1 +sg7 +Vcd2009e3bf334527a81216c4ed00f455.16 +p177 +sa(dp178 +g2 +L1469922734509L +sg3 +I01 +sg4 +F-5.986344 +sg5 +F37.393054 +sg6 +I1 +sg7 +Vd81d562a7e57448fb7a7f074bd419e98.16 +p179 +sa(dp180 +g2 +L1469824608332L +sg3 +I01 +sg4 +F-5.987611 +sg5 +F37.393942 +sg6 +I1 +sg7 +Ve328724e66eb4ce7a0adeee5a6a40127.16 +p181 +sa(dp182 +g2 +L1469826878797L +sg3 +I01 +sg4 +F-5.986986 +sg5 +F37.391947 +sg6 +I1 +sg7 +Vf8863770b4774049b7557dd85fbda4f1.16 +p183 +sa(dp184 +g2 +L1469801146432L +sg3 +I01 +sg4 +F-5.984757 +sg5 +F37.399149 +sg6 +I1 +sg7 +V09cb0b980c2942b783b5e8f3f907428e.16 +p185 +sa(dp186 +g2 +L1469920128121L +sg3 +I01 +sg4 +F-5.985529 +sg5 +F37.399078 +sg6 +I1 +sg7 +V18d3084c92cc4dd4ba27cc3eaf7dd315.16 +p187 +sa(dp188 +g2 +L1469900596799L +sg3 +I01 +sg4 +F-5.983342 +sg5 +F37.397584 +sg6 +I1 +sg7 +V5efdf524b7c24b5484be94252f355638.16 +p189 +sa(dp190 +g2 +L1469920135971L +sg3 +I01 +sg4 +F-5.985615 +sg5 +F37.398821 +sg6 +I1 +sg7 +V73d88cf75bc542e5b86c1f50fbfc13e1.11 +p191 +sa(dp192 +g2 +L1469892832049L +sg3 +I01 +sg4 +F-5.984188 +sg5 +F37.397536 +sg6 +I1 +sg7 +V8758bdd8f9774ec2bc2f1272faa81198.16 +p193 +sa(dp194 +g2 +L1469960321243L +sg3 +I01 +sg4 +F-6.002275 +sg5 +F37.397373 +sg6 +I1 +sg7 +V38603d571ef14e93ae94ee3b95366f04.16 +p195 +sa(dp196 +g2 +L1469912143387L +sg3 +I01 +sg4 +F-6.001769 +sg5 +F37.398108 +sg6 +I1 +sg7 +Va4ff6d35307c4994b0e3106805a990f4.16 +p197 +sa(dp198 +g2 +L1469293296262L +sg3 +I01 +sg4 +F-5.983975 +sg5 +F37.394835 +sg6 +I1 +sg7 +Vbbe12779551b4dacaf7e0ed219dc841a.16 +p199 +sa(dp200 +g2 +L1469539921656L +sg3 +I01 +sg4 +F-5.984555 +sg5 +F37.39584 +sg6 +I1 +sg7 +Vdda84e7d0aa64b74b74adfe730058cde.16 +p201 +sa(dp202 +g2 +L1469450065604L +sg3 +I01 +sg4 +F-5.985628 +sg5 +F37.395615 +sg6 +I1 +sg7 +Vee073b5551df4b4b82c2e1dbd62394ae.16 +p203 +sa(dp204 +g2 +L1469916347191L +sg3 +I01 +sg4 +F-5.985723 +sg5 +F37.392219 +sg6 +I1 +sg7 +V6024589481e24862a458d6e131fc88d4.16 +p205 +sa(dp206 +g2 +L1469904426841L +sg3 +I01 +sg4 +F-5.983613 +sg5 +F37.394012 +sg6 +I1 +sg7 +V65237b304b404cef8ea8b65b1f9ebd95.16 +p207 +sa. \ No newline at end of file diff --git a/pokemongo_bot/test/sleep_schedule_test.py b/pokemongo_bot/test/sleep_schedule_test.py new file mode 100644 index 0000000000..05c35afbb6 --- /dev/null +++ b/pokemongo_bot/test/sleep_schedule_test.py @@ -0,0 +1,107 @@ +import unittest +from datetime import timedelta, datetime +from mock import patch, MagicMock +from pokemongo_bot.cell_workers.sleep_schedule import SleepSchedule +from tests import FakeBot + + +class SleepScheculeTestCase(unittest.TestCase): + config = {'time': '12:20', 'duration': '01:05', 'time_random_offset': '00:05', 'duration_random_offset': '00:05'} + + def setUp(self): + self.bot = FakeBot() + self.worker = SleepSchedule(self.bot, self.config) + + def test_config(self): + self.assertEqual(self.worker.time.hour, 12) + self.assertEqual(self.worker.time.minute, 20) + self.assertEqual(self.worker.duration, timedelta(hours=1, minutes=5).total_seconds()) + self.assertEqual(self.worker.time_random_offset, timedelta(minutes=5).total_seconds()) + self.assertEqual(self.worker.duration_random_offset, timedelta(minutes=5).total_seconds()) + + @patch('pokemongo_bot.cell_workers.sleep_schedule.datetime') + def test_get_next_time(self, mock_datetime): + mock_datetime.now.return_value = datetime(year=2016, month=8, day=01, hour=8, minute=0) + + next_time = self.worker._get_next_sleep_schedule() + from_date = datetime(year=2016, month=8, day=1, hour=12, minute=15) + to_date = datetime(year=2016, month=8, day=1, hour=12, minute=25) + + self.assertGreaterEqual(next_time, from_date) + self.assertLessEqual(next_time, to_date) + + @patch('pokemongo_bot.cell_workers.sleep_schedule.datetime') + def test_get_next_time_called_near_activation_time(self, mock_datetime): + mock_datetime.now.return_value = datetime(year=2016, month=8, day=1, hour=12, minute=25) + + next = self.worker._get_next_sleep_schedule() + from_date = datetime(year=2016, month=8, day=02, hour=12, minute=15) + to_date = datetime(year=2016, month=8, day=02, hour=12, minute=25) + + self.assertGreaterEqual(next, from_date) + self.assertLessEqual(next, to_date) + + @patch('pokemongo_bot.cell_workers.sleep_schedule.datetime') + def test_get_next_time_called_when_this_days_time_passed(self, mock_datetime): + mock_datetime.now.return_value = datetime(year=2016, month=8, day=1, hour=14, minute=0) + + next = self.worker._get_next_sleep_schedule() + from_date = datetime(year=2016, month=8, day=02, hour=12, minute=15) + to_date = datetime(year=2016, month=8, day=02, hour=12, minute=25) + + self.assertGreaterEqual(next, from_date) + self.assertLessEqual(next, to_date) + + def test_get_next_duration(self): + from_seconds = int(timedelta(hours=1).total_seconds()) + to_seconds = int(timedelta(hours=1, minutes=10).total_seconds()) + + duration = self.worker._get_next_duration() + + self.assertGreaterEqual(duration, from_seconds) + self.assertLessEqual(duration, to_seconds) + + @patch('pokemongo_bot.cell_workers.sleep_schedule.sleep') + def test_sleep(self, mock_sleep): + self.worker._next_duration = SleepSchedule.LOG_INTERVAL_SECONDS * 10 + self.worker._sleep() + #Sleep should be called 10 times with LOG_INTERVAL_SECONDS as argument + self.assertEqual(mock_sleep.call_count, 10) + calls = [x[0][0] for x in mock_sleep.call_args_list] + for arg in calls: + self.assertEqual(arg, SleepSchedule.LOG_INTERVAL_SECONDS) + + @patch('pokemongo_bot.cell_workers.sleep_schedule.sleep') + def test_sleep_not_divedable_by_interval(self, mock_sleep): + self.worker._next_duration = SleepSchedule.LOG_INTERVAL_SECONDS * 10 + 5 + self.worker._sleep() + self.assertEqual(mock_sleep.call_count, 11) + + calls = [x[0][0] for x in mock_sleep.call_args_list] + for arg in calls[:-1]: + self.assertEqual(arg, SleepSchedule.LOG_INTERVAL_SECONDS) + #Last call must be 5 + self.assertEqual(calls[-1], 5) + + @patch('pokemongo_bot.cell_workers.sleep_schedule.sleep') + @patch('pokemongo_bot.cell_workers.sleep_schedule.datetime') + def test_call_work_before_schedule(self, mock_datetime, mock_sleep): + self.worker._next_sleep = datetime(year=2016, month=8, day=1, hour=12, minute=0) + mock_datetime.now.return_value = self.worker._next_sleep - timedelta(minutes=5) + + self.worker.work() + + self.assertEqual(mock_sleep.call_count, 0) + + @patch('pokemongo_bot.cell_workers.sleep_schedule.sleep') + @patch('pokemongo_bot.cell_workers.sleep_schedule.datetime') + def test_call_work_after_schedule(self, mock_datetime, mock_sleep): + self.bot.login = MagicMock() + self.worker._next_sleep = datetime(year=2016, month=8, day=1, hour=12, minute=0) + # Change time to be after schedule + mock_datetime.now.return_value = self.worker._next_sleep + timedelta(minutes=5) + + self.worker.work() + + self.assertGreater(mock_sleep.call_count, 0) + self.assertGreater(self.bot.login.call_count, 0) diff --git a/pokemongo_bot/test/socketio-client.py b/pokemongo_bot/test/socketio-client.py new file mode 100644 index 0000000000..d30feee58d --- /dev/null +++ b/pokemongo_bot/test/socketio-client.py @@ -0,0 +1,15 @@ +from socketIO_client import SocketIO + + +def on_location(msg): + print('received location: {}'.format(msg)) + +if __name__ == "__main__": + try: + socketio = SocketIO('localhost', 4000) + socketio.on('location', on_location) + while True: + socketio.wait(seconds=5) + + except (KeyboardInterrupt, SystemExit): + print "Exiting" diff --git a/pokemongo_bot/tree_config_builder.py b/pokemongo_bot/tree_config_builder.py new file mode 100644 index 0000000000..bba63ad880 --- /dev/null +++ b/pokemongo_bot/tree_config_builder.py @@ -0,0 +1,36 @@ +import cell_workers + +class ConfigException(Exception): + pass + +class TreeConfigBuilder(object): + def __init__(self, bot, tasks_raw): + self.bot = bot + self.tasks_raw = tasks_raw + + def _get_worker_by_name(self, name): + try: + worker = getattr(cell_workers, name) + except AttributeError: + raise ConfigException('No worker named {} defined'.format(name)) + + return worker + + def build(self): + workers = [] + + for task in self.tasks_raw: + task_type = task.get('type', None) + if task_type is None: + raise ConfigException('No type found for given task {}'.format(task)) + elif task_type == 'EvolveAll': + raise ConfigException('The EvolveAll task has been renamed to EvolvePokemon') + + task_config = task.get('config', {}) + + worker = self._get_worker_by_name(task_type) + instance = worker(self.bot, task_config) + workers.append(instance) + + return workers + diff --git a/pokemongo_bot/polyline_walker/__init__.py b/pokemongo_bot/walkers/__init__.py similarity index 51% rename from pokemongo_bot/polyline_walker/__init__.py rename to pokemongo_bot/walkers/__init__.py index 272b8375d4..c7021b6d56 100644 --- a/pokemongo_bot/polyline_walker/__init__.py +++ b/pokemongo_bot/walkers/__init__.py @@ -1 +1,2 @@ +from polyline_generator import Polyline from polyline_walker import PolylineWalker diff --git a/pokemongo_bot/polyline_walker/polyline_walker.py b/pokemongo_bot/walkers/polyline_generator.py similarity index 51% rename from pokemongo_bot/polyline_walker/polyline_walker.py rename to pokemongo_bot/walkers/polyline_generator.py index e6a92c1c5e..ee225cb9e8 100644 --- a/pokemongo_bot/polyline_walker/polyline_walker.py +++ b/pokemongo_bot/walkers/polyline_generator.py @@ -1,23 +1,29 @@ -import requests -import polyline -import haversine import time -from itertools import chain +from itertools import chain from math import ceil -class PolylineWalker(object): +import haversine +import polyline +import requests + + +class Polyline(object): def __init__(self, origin, destination, speed): self.DISTANCE_API_URL='https://maps.googleapis.com/maps/api/directions/json?mode=walking' self.origin = origin self.destination = destination - self.polyline_points = [x['polyline']['points'] for x in - requests.get(self.DISTANCE_API_URL+'&origin='+ - self.origin+'&destination='+ - self.destination - ).json()['routes'][0]['legs'][0]['steps']] + self.URL = '{}&origin={}&destination={}'.format(self.DISTANCE_API_URL, + '{},{}'.format(*self.origin), + '{},{}'.format(*self.destination)) + self.request_responce = requests.get(self.URL).json() + try: + self.polyline_points = [x['polyline']['points'] for x in + self.request_responce['routes'][0]['legs'][0]['steps']] + except IndexError: + self.polyline_points = self.request_responce['routes'] self.speed = float(speed) - self.points = self.get_points(self.polyline_points) + self.points = [self.origin] + self.get_points(self.polyline_points) + [self.destination] self.lat, self.long = self.points[0][0], self.points[0][1] self.polyline = self.combine_polylines(self.points) self._timestamp = time.time() @@ -57,7 +63,8 @@ def walk_steps(self): walk_steps = zip(chain([self.points[0]], self.points), chain(self.points, [self.points[-1]])) walk_steps = filter(None, [(o, d) if o != d else None for o, d in walk_steps]) - return walk_steps + # consume the filter as list https://github.com/th3w4y/PokemonGo-Bot/issues/27 + return list(walk_steps) else: return [] @@ -68,26 +75,35 @@ def get_pos(self): else: time_passed = self._last_paused_timestamp time_passed_distance = self.speed * abs(time_passed - self._timestamp - self._paused_total) - steps_dict = {} - for step in self.walk_steps(): - walked_distance += haversine.haversine(*step)*1000 - steps_dict[walked_distance] = step - for walked_end_step in sorted(steps_dict.keys()): + # check if there are any steps to take https://github.com/th3w4y/PokemonGo-Bot/issues/27 + if self.walk_steps(): + steps_dict = {} + for step in self.walk_steps(): + walked_distance += haversine.haversine(*step)*1000 + steps_dict[walked_distance] = step + for walked_end_step in sorted(steps_dict.keys()): + if walked_end_step >= time_passed_distance: + break + step_distance = haversine.haversine(*steps_dict[walked_end_step])*1000 if walked_end_step >= time_passed_distance: - break - step_distance = haversine.haversine(*steps_dict[walked_end_step])*1000 - if walked_end_step >= time_passed_distance: - percentage_walked = (time_passed_distance - (walked_end_step - step_distance)) / step_distance + percentage_walked = (time_passed_distance - (walked_end_step - step_distance)) / step_distance + else: + percentage_walked = 1.0 + return self.calculate_coord(percentage_walked, *steps_dict[walked_end_step]) else: - percentage_walked = 1.0 - return self.calculate_coord(percentage_walked, *steps_dict[walked_end_step]) + # otherwise return the destination https://github.com/th3w4y/PokemonGo-Bot/issues/27 + return [self.points[-1]] def calculate_coord(self, percentage, o, d): - lat = o[0]+ (d[0] -o[0]) * percentage - lon = o[1]+ (d[1] -o[1]) * percentage - return [(round(lat, 5), round(lon, 5))] + # If this is the destination then returning as such + if self.points[-1] == d: + return [d] + else: + # intermediary points returned with 5 decimals precision only + # this ensures ~3-50cm ofset from the geometrical point calculated + lat = o[0]+ (d[0] -o[0]) * percentage + lon = o[1]+ (d[1] -o[1]) * percentage + return [(round(lat, 5), round(lon, 5))] def get_total_distance(self): return ceil(sum([haversine.haversine(*x)*1000 for x in self.walk_steps()])) - - diff --git a/pokemongo_bot/polyline_walker/polyline_tester.py b/pokemongo_bot/walkers/polyline_generator_tester.py similarity index 83% rename from pokemongo_bot/polyline_walker/polyline_tester.py rename to pokemongo_bot/walkers/polyline_generator_tester.py index fdf547fdd6..72852eebb9 100644 --- a/pokemongo_bot/polyline_walker/polyline_tester.py +++ b/pokemongo_bot/walkers/polyline_generator_tester.py @@ -1,9 +1,13 @@ import time +from math import ceil + import haversine import polyline -from math import ceil -from polyline_walker import PolylineWalker -a = PolylineWalker('Poststrasse+20,Zug,CH', 'Guggiweg+7,Zug,CH', 100) + +from polyline_generator import Polyline + +a = Polyline((47.1706378, 8.5167405), (47.1700271, 8.518072999999998), 100) +print(a.points) print('Walking polyline: ', a.polyline) print('Encoded level: ','B'*len(a.points)) print('Initialted with speed: ', a.speed, 'm/s') diff --git a/pokemongo_bot/walkers/polyline_walker.py b/pokemongo_bot/walkers/polyline_walker.py new file mode 100644 index 0000000000..687371e866 --- /dev/null +++ b/pokemongo_bot/walkers/polyline_walker.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.step_walker import StepWalker +from polyline_generator import Polyline + + +class PolylineWalker(StepWalker): + + def __init__(self, bot, speed, dest_lat, dest_lng): + super(PolylineWalker, self).__init__(bot, speed, dest_lat, dest_lng) + self.polyline_walker = Polyline((self.api._position_lat, self.api._position_lng), + (self.destLat, self.destLng), self.speed) + self.bot.event_manager.emit( + 'polyline_request', + sender=self, + level='info', + formatted="{url}", + data={'url': self.polyline_walker.URL} + ) + + def step(self): + cLat, cLng = self.api._position_lat, self.api._position_lng + while (cLat, cLng) != self.polyline_walker.get_pos()[0]: + self.polyline_walker.unpause() + sleep(1) + self.polyline_walker.pause() + cLat, cLng = self.polyline_walker.get_pos()[0] + self.api.set_position(round(cLat, 5), round(cLng, 5), 0) + self.bot.heartbeat() + return True diff --git a/pokemongo_bot/websocket_remote_control.py b/pokemongo_bot/websocket_remote_control.py new file mode 100644 index 0000000000..c4e15362b6 --- /dev/null +++ b/pokemongo_bot/websocket_remote_control.py @@ -0,0 +1,53 @@ +import threading +from socketIO_client import SocketIO, BaseNamespace + + +class WebsocketRemoteControl(object): + + + def __init__(self, bot): + self.bot = bot + self.host, port_str = self.bot.config.websocket_server_url.split(':') + self.port = int(port_str) + self.sio = SocketIO(self.host, self.port) + self.sio.on( + 'bot:process_request:{}'.format(self.bot.config.username), + self.on_remote_command + ) + self.thread = threading.Thread(target=self.process_messages) + + def start(self): + self.thread.start() + return self + + def process_messages(self): + self.sio.wait() + + def on_remote_command(self, command): + name = command['name'] + command_handler = getattr(self, name, None) + if not command_handler or not callable(command_handler): + self.sio.emit( + 'bot:send_reply', + { + 'response': '', + 'command': 'command_not_found', + 'account': self.bot.config.username + } + ) + return + if 'args' in command: + command_handler(*args) + return + command_handler() + + def get_player_info(self): + player_info = self.bot.get_inventory()['responses']['GET_INVENTORY'] + self.sio.emit( + 'bot:send_reply', + { + 'result': player_info, + 'command': 'get_player_info', + 'account': self.bot.config.username + } + ) diff --git a/pokemongo_bot/worker_result.py b/pokemongo_bot/worker_result.py new file mode 100644 index 0000000000..f38ceb9704 --- /dev/null +++ b/pokemongo_bot/worker_result.py @@ -0,0 +1,3 @@ +class WorkerResult(object): + RUNNING = 'RUNNING' + SUCCESS = 'SUCCESS' diff --git a/pylint-recursive.py b/pylint-recursive.py new file mode 100644 index 0000000000..82c4664e97 --- /dev/null +++ b/pylint-recursive.py @@ -0,0 +1,60 @@ +#! /usr/bin/env python +''' +Author: gregorynicholas (github), modified by Jacob Henderson (jacohend, github) +Module that runs pylint on all python scripts found in a directory tree.. +''' + +import os +#import re +import sys + +passed = 0 +failed = 0 +errors = list() + +IGNORED_FILES = ["lcd.py"] + +def check(module): + global passed, failed + ''' + apply pylint to the file specified if it is a *.py file + ''' + module_name = module.rsplit('/', 1)[1] + if module[-3:] == ".py" and module_name not in IGNORED_FILES: + print "CHECKING ", module + pout = os.popen('pylint %s'% module, 'r') + for line in pout: + if "Your code has been rated at" in line: + print "PASSED pylint inspection: " + line + passed += 1 + return True + if "-error" in line: + print "FAILED pylint inspection: " + line + failed += 1 + errors.append("FILE: " + module) + errors.append("FAILED pylint inspection: " + line) + return False + +if __name__ == "__main__": + try: + print sys.argv + BASE_DIRECTORY = sys.argv[1] + except IndexError: + print "no directory specified, defaulting to current working directory" + BASE_DIRECTORY = os.getcwd() + + print "looking for *.py scripts in subdirectories of ", BASE_DIRECTORY + + for root, dirs, files in os.walk(BASE_DIRECTORY): + for name in files: + filepath = os.path.join(root, name) + check(filepath) + + print "Passed: " + str(passed) + " Failed: " + str(failed) + print "\n" + print "Showing errors:" + if failed > 0: + for err in errors: + print err + + sys.exit("Pylint failed with errors") diff --git a/release_config.json.example b/release_config.json.example deleted file mode 100644 index 06d4687e9a..0000000000 --- a/release_config.json.example +++ /dev/null @@ -1,174 +0,0 @@ -{ - "any": { - "release_under_cp": 400, - "release_under_iv": 0.9, - "cp_iv_logic": "and" - }, - - "Bulbasaur": { "release_under_cp": 374, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Ivysaur": { "release_under_cp": 571, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Venusaur": { "release_under_cp": 902, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Charmander": { "release_under_cp": 333, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Charmeleon": { "release_under_cp": 544, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Charizard": { "release_under_cp": 909, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Squirtle": { "release_under_cp": 352, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Wartortle": { "release_under_cp": 552, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Blastoise": { "release_under_cp": 888, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Caterpie": { "release_under_cp": 156, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Metapod": { "release_under_cp": 168, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Butterfree": { "release_under_cp": 508, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Weedle": { "release_under_cp": 156, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Kakuna": { "release_under_cp": 170, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Beedrill": { "release_under_cp": 504, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Pidgey": { "release_under_cp": 237, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Pidgeotto": { "release_under_cp": 427, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Pidgeot": { "release_under_cp": 729, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Rattata": { "release_under_cp": 204, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Raticate": { "release_under_cp": 504, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Spearow": { "release_under_cp": 240, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Fearow": { "release_under_cp": 609, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Ekans": { "release_under_cp": 288, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Arbok": { "release_under_cp": 616, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Pikachu": { "release_under_cp": 309, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Raichu": { "release_under_cp": 708, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Sandshrew": { "release_under_cp": 278, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Sandslash": { "release_under_cp": 631, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Nidoran F": { "release_under_cp": 304, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Nidorina": { "release_under_cp": 489, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Nidoqueen": { "release_under_cp": 868, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Nidoran M": { "release_under_cp": 295, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Nidorino": { "release_under_cp": 480, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Nidoking": { "release_under_cp": 864, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Clefairy": { "release_under_cp": 420, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Clefable": { "release_under_cp": 837, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Vulpix": { "release_under_cp": 290, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Ninetales": { "release_under_cp": 763, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Jigglypuff": { "release_under_cp": 321, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Wigglytuff": { "release_under_cp": 760, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Zubat": { "release_under_cp": 225, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Golbat": { "release_under_cp": 672, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Oddish": { "release_under_cp": 400, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Gloom": { "release_under_cp": 590, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Vileplume": { "release_under_cp": 871, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Paras": { "release_under_cp": 319, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Parasect": { "release_under_cp": 609, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Venonat": { "release_under_cp": 360, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Venomoth": { "release_under_cp": 660, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Diglett": { "release_under_cp": 158, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Dugtrio": { "release_under_cp": 408, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Meowth": { "release_under_cp": 264, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Persian": { "release_under_cp": 568, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Psyduck": { "release_under_cp": 386, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Golduck": { "release_under_cp": 832, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Mankey": { "release_under_cp": 307, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Primeape": { "release_under_cp": 650, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Growlithe": { "release_under_cp": 465, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Arcanine": { "release_under_cp": 1041, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Poliwag": { "release_under_cp": 278, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Poliwhirl": { "release_under_cp": 468, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Poliwrath": { "release_under_cp": 876, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Abra": { "release_under_cp": 208, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Kadabra": { "release_under_cp": 396, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Alakazam": { "release_under_cp": 633, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Machop": { "release_under_cp": 381, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Machoke": { "release_under_cp": 614, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Machamp": { "release_under_cp": 907, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Bellsprout": { "release_under_cp": 391, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Weepinbell": { "release_under_cp": 602, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Victreebel": { "release_under_cp": 883, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Tentacool": { "release_under_cp": 316, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Tentacruel": { "release_under_cp": 775, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Geodude": { "release_under_cp": 297, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Graveler": { "release_under_cp": 501, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Golem": { "release_under_cp": 804, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Ponyta": { "release_under_cp": 530, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Rapidash": { "release_under_cp": 768, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Slowpoke": { "release_under_cp": 424, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Slowbro": { "release_under_cp": 907, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Magnemite": { "release_under_cp": 312, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Magneton": { "release_under_cp": 657, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Farfetch'd": { "release_under_cp": 441, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Doduo": { "release_under_cp": 297, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Dodrio": { "release_under_cp": 640, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Seel": { "release_under_cp": 386, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Dewgong": { "release_under_cp": 748, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Grimer": { "release_under_cp": 448, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Muk": { "release_under_cp": 909, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Shellder": { "release_under_cp": 288, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Cloyster": { "release_under_cp": 717, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Gastly": { "release_under_cp": 280, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Haunter": { "release_under_cp": 482, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Gengar": { "release_under_cp": 724, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Onix": { "release_under_cp": 300, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Drowzee": { "release_under_cp": 374, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Hypno": { "release_under_cp": 763, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Krabby": { "release_under_cp": 276, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Kingler": { "release_under_cp": 636, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Voltorb": { "release_under_cp": 292, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Electrode": { "release_under_cp": 576, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Exeggcute": { "release_under_cp": 384, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Exeggutor": { "release_under_cp": 1032, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Cubone": { "release_under_cp": 352, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Marowak": { "release_under_cp": 578, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Hitmonlee": { "release_under_cp": 520, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Hitmonchan": { "release_under_cp": 530, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Lickitung": { "release_under_cp": 568, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Koffing": { "release_under_cp": 403, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Weezing": { "release_under_cp": 784, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Rhyhorn": { "release_under_cp": 412, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Rhydon": { "release_under_cp": 782, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Chansey": { "release_under_cp": 235, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Tangela": { "release_under_cp": 607, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Kangaskhan": { "release_under_cp": 712, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Horsea": { "release_under_cp": 278, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Seadra": { "release_under_cp": 597, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Goldeen": { "release_under_cp": 336, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Seaking": { "release_under_cp": 712, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Staryu": { "release_under_cp": 326, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Starmie": { "release_under_cp": 763, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Mr. Mime": { "release_under_cp": 520, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Scyther": { "release_under_cp": 724, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Jynx": { "release_under_cp": 600, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Electabuzz": { "release_under_cp": 739, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Magmar": { "release_under_cp": 792, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Pinsir": { "release_under_cp": 741, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Tauros": { "release_under_cp": 643, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Magikarp": { "release_under_cp": 91, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Gyarados": { "release_under_cp": 938, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Lapras": { "release_under_cp": 1041, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Ditto": { "release_under_cp": 321, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Eevee": { "release_under_cp": 376, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Vaporeon": { "release_under_cp": 984, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Jolteon": { "release_under_cp": 746, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Flareon": { "release_under_cp": 924, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Porygon": { "release_under_cp": 590, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Omanyte": { "release_under_cp": 391, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Omastar": { "release_under_cp": 780, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Kabuto": { "release_under_cp": 386, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Kabutops": { "release_under_cp": 744, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Aerodactyl": { "release_under_cp": 756, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Snorlax": { "release_under_cp": 1087, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Articuno": { "release_under_cp": 1039, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Zapdos": { "release_under_cp": 1087, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Moltres": { "release_under_cp": 1132, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Dratini": { "release_under_cp": 343, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Dragonair": { "release_under_cp": 609, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Dragonite": { "release_under_cp": 1221, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Mewtwo": { "release_under_cp": 1447, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - "Mew": { "release_under_cp": 1152, "release_under_iv": 0.8, "cp_iv_logic": "and" }, - - "exceptions": { - "always_capture": [ - "Arcanine", - "Lapras", - "Dragonite", - "Snorlax", - "Blastoise", - "Moltres", - "Articuno", - "Zapdos", - "Mew", - "Mewtwo" - ] - } -} diff --git a/requirements.txt b/requirements.txt index 5c5223bf71..d8b5ce5c9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ --e git+https://github.com/tejado/pgoapi.git@3787ffbe2e80ebce8a02d48eebceb9edf40179c1#egg=pgoapi +numpy==1.11.0 +networkx==1.11 +-e git+https://github.com/tejado/pgoapi.git@0811db23d639039f968a82e06c7aa15a0a5016b6#egg=pgoapi geopy==1.11.0 protobuf==3.0.0b4 requests==2.10.0 @@ -11,3 +13,11 @@ enum34==1.1.6 pyyaml==3.11 haversine==0.4.5 polyline==1.3.1 +python-socketio==1.4.2 +flask==0.11.1 +socketIO_client==0.7.0 +eventlet==0.19.0 +gpxpy==1.1.1 +mock==2.0.0 +timeout-decorator==0.3.2 +raven==5.23.0 diff --git a/run.sh b/run.sh new file mode 100755 index 0000000000..ec95acb3e3 --- /dev/null +++ b/run.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# Starts PokemonGo-Bot +config="" + +if [ ! -z $1 ]; then + config=$1 +else + config="./configs/config.json" + if [ ! -f ${config} ]; then + echo -e "There's no ./configs/config.json file" + echo -e "Please create one or use another config file" + echo -e "./run.sh [path/to/config/file]" + exit 1 + fi +fi + +python pokecli.py --config ${config} diff --git a/setup.py b/setup.py index f0a267c497..6ccf893c0d 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ #!/usr/bin/env python -from distutils.core import setup from pip.req import parse_requirements install_reqs = parse_requirements("requirements.txt", session=False) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..c02aed60fc --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,23 @@ +# __init__.py +from mock import MagicMock + +from pokemongo_bot.event_manager import EventManager +from pokemongo_bot.api_wrapper import ApiWrapper, ApiRequest +from pokemongo_bot import PokemonGoBot + +class FakeApi(ApiWrapper): + def create_request(self, return_value='mock return'): + request = ApiWrapper.create_request(self) + request.can_call = MagicMock(return_value=True) + request._call = MagicMock(return_value=return_value) + return request + +class FakeBot(PokemonGoBot): + def __init__(self): + self.config = MagicMock(websocket_server_url=False, show_events=False) + self.api = FakeApi() + self.event_manager = EventManager() + self._setup_event_system() + + def updateConfig(self, conf): + self.config.__dict__.update(conf) diff --git a/tests/api_wrapper_test.py b/tests/api_wrapper_test.py new file mode 100644 index 0000000000..335f04cbf2 --- /dev/null +++ b/tests/api_wrapper_test.py @@ -0,0 +1,123 @@ +import unittest +from mock import MagicMock, patch +from timeout_decorator import timeout, TimeoutError + +from tests import FakeApi + +from pgoapi import PGoApi +from pgoapi.exceptions import NotLoggedInException, ServerBusyOrOfflineException, NoPlayerPositionSetException, EmptySubrequestChainException +from pokemongo_bot.api_wrapper import ApiWrapper + +class TestApiWrapper(unittest.TestCase): + def test_raises_not_logged_in_exception(self): + api = ApiWrapper() + api.set_position(*(42, 42, 0)) + request = api.create_request() + request.get_inventory(test='awesome') + with self.assertRaises(NotLoggedInException): + request.call() + + def test_api_call_with_no_requests_set(self): + request = ApiWrapper().create_request() + with self.assertRaises(EmptySubrequestChainException): + request.call() + + def test_api_wrong_request(self): + request = ApiWrapper().create_request() + with self.assertRaises(AttributeError): + request.wrong_request() + + def test_raises_no_player_position_set_exception(self): + request = ApiWrapper().create_request() + request.get_inventory(test='awesome') + with self.assertRaises(NoPlayerPositionSetException): + request.call() + + @patch('pokemongo_bot.api_wrapper.sleep') + def test_api_server_is_unreachable_raises_server_busy_or_offline_exception(self, sleep): + sleep.return_value = True # we don't need to really sleep + request = FakeApi().create_request('Wrong Value') + request.get_inventory() + # we expect an exception because the "server" isn't returning a valid response + with self.assertRaises(ServerBusyOrOfflineException): + request.call() + + def test_mocked_call(self): + request = FakeApi().create_request(True) + request.is_response_valid = MagicMock(return_value=True) + request.get_inventory(test='awesome') + result = request.call() + self.assertTrue(result) + + def test_return_value_is_not_valid(self): + api = FakeApi() + def returnRequest(ret_value): + request = api.create_request(ret_value) + request.get_inventory(test='awesome') + return request + + wrong_return_values = [ + None, + False, + {}, + {'responses': {}}, + {'status_code': 0}, + {'responses': {'GET_INVENTORY_OR_NOT': {}}, 'status_code': 0} + ] + for wrong in wrong_return_values: + request = returnRequest(wrong) + request_callers = request._pop_request_callers() # we can pop because we do no call + + is_valid = request.is_response_valid(wrong, request_callers) + self.assertFalse(is_valid, 'return value {} is valid somehow ?'.format(wrong)) + + def test_return_value_is_valid(self): + request = FakeApi().create_request() # we set the return value below + request.get_inventory(test='awesome') + + request_caller = request.request_callers[0] # only one request + self.assertEqual(request_caller.upper(), 'GET_INVENTORY') + + good_return_value = {'responses': {request_caller.upper(): {}}, 'status_code': 0} + request._call.return_value = good_return_value + + result = request.call() + self.assertEqual(result, good_return_value) + self.assertEqual(len(request.request_callers), 0, 'request_callers must be empty') + + def test_multiple_requests(self): + request = FakeApi().create_request() + request.get_inventory(test='awesome') + request.fort_details() + + good_return_value = {'responses': {'GET_INVENTORY': {}, 'FORT_DETAILS': {}}, 'status_code': 0} + request._call.return_value = good_return_value + + result = request.call() + self.assertEqual(result, good_return_value) + + @timeout(1) + def test_api_call_throttle_should_pass(self): + request = FakeApi().create_request() + request.is_response_valid = MagicMock(return_value=True) + request.requests_per_seconds = 5 + + for i in range(request.requests_per_seconds): + request.call() + + @timeout(1) # expects a timeout + def test_api_call_throttle_should_fail(self): + request = FakeApi().create_request() + request.is_response_valid = MagicMock(return_value=True) + request.requests_per_seconds = 5 + + with self.assertRaises(TimeoutError): + for i in range(request.requests_per_seconds * 2): + request.call() + + @patch('pokemongo_bot.api_wrapper.ApiRequest.is_response_valid') + def test_api_direct_call(self, mock_method): + mock_method.return_value = True + + result = FakeApi().get_inventory() + self.assertEqual(result, 'mock return') diff --git a/tests/base_task_test.py b/tests/base_task_test.py new file mode 100644 index 0000000000..16684d900c --- /dev/null +++ b/tests/base_task_test.py @@ -0,0 +1,40 @@ +import unittest +import json +from pokemongo_bot.cell_workers import BaseTask + +class FakeTask(BaseTask): + def initialize(self): + self.foo = 'foo' + + def work(self): + pass + +class FakeTaskWithoutInitialize(BaseTask): + def work(self): + pass + +class FakeTaskWithoutWork(BaseTask): + pass + +class BaseTaskTest(unittest.TestCase): + def setUp(self): + self.bot = {} + self.config = {} + + def test_initialize_called(self): + task = FakeTask(self.bot, self.config) + self.assertIs(task.bot, self.bot) + self.assertIs(task.config, self.config) + self.assertEquals(task.foo, 'foo') + + def test_does_not_throw_without_initialize(self): + FakeTaskWithoutInitialize(self.bot, self.config) + + def test_throws_without_work(self): + self.assertRaisesRegexp( + NotImplementedError, + 'Missing "work" method', + FakeTaskWithoutWork, + self.bot, + self.config + ) diff --git a/tests/location_parser_test.py b/tests/location_parser_test.py new file mode 100644 index 0000000000..e724adebf8 --- /dev/null +++ b/tests/location_parser_test.py @@ -0,0 +1,33 @@ +# coding: utf-8 +import unittest +from mock import MagicMock + +from geopy.exc import GeocoderQueryError +from tests import FakeBot + + +class TestLocationParser(unittest.TestCase): + + def setUp(self): + self.bot = FakeBot() + config = dict( + test=False, + location='Paris', + location_cache=False, + username='Foobar', + ) + self.bot.updateConfig(config) + + def test_named_position(self): + position = (42, 42, 0) + self.bot.get_pos_by_name = MagicMock(return_value=position) + self.bot._set_starting_position() + self.assertEqual(self.bot.position, position) + + def test_named_position_utf8(self): + position = (42, 42, 0) + self.bot.config.location = u"àéùƱǣЊ؍ ข᠃" + self.bot.get_pos_by_name = MagicMock(return_value=position) + + self.bot._set_starting_position() + self.assertEqual(self.bot.position, position) diff --git a/tests/step_walker_test.py b/tests/step_walker_test.py new file mode 100644 index 0000000000..7472953ac6 --- /dev/null +++ b/tests/step_walker_test.py @@ -0,0 +1,73 @@ +import unittest +from mock import MagicMock, patch + +from pokemongo_bot.step_walker import StepWalker +from pokemongo_bot.cell_workers.utils import float_equal + +NORMALIZED_LAT_LNG_DISTANCE_STEP = 6.3593e-6 + +class TestStepWalker(unittest.TestCase): + def setUp(self): + self.patcherSleep = patch('pokemongo_bot.step_walker.sleep') + self.patcherRandomLat = patch('pokemongo_bot.step_walker.random_lat_long_delta', return_value=0) + self.patcherSleep.start() + self.patcherRandomLat.start() + + self.bot = MagicMock() + self.bot.position = [0, 0, 0] + self.bot.api = MagicMock() + + self.lat, self.lng, self.alt = 0, 0, 0 + + # let us get back the position set by the StepWalker + def api_set_position(lat, lng, alt): + self.lat, self.lng, self.alt = lat, lng, alt + self.bot.api.set_position = api_set_position + + def tearDown(self): + self.patcherSleep.stop() + self.patcherRandomLat.stop() + + def test_normalized_distance(self): + sw = StepWalker(self.bot, 1, 0.1, 0.1) + self.assertGreater(sw.dLat, 0) + self.assertGreater(sw.dLng, 0) + + stayInPlace = sw.step() + self.assertFalse(stayInPlace) + + self.assertTrue(float_equal(self.lat, NORMALIZED_LAT_LNG_DISTANCE_STEP)) + self.assertTrue(float_equal(self.lng, NORMALIZED_LAT_LNG_DISTANCE_STEP)) + + def test_normalized_distance_times_2(self): + sw = StepWalker(self.bot, 2, 0.1, 0.1) + self.assertTrue(sw.dLat > 0) + self.assertTrue(sw.dLng > 0) + + stayInPlace = sw.step() + self.assertFalse(stayInPlace) + + self.assertTrue(float_equal(self.lat, NORMALIZED_LAT_LNG_DISTANCE_STEP * 2)) + self.assertTrue(float_equal(self.lng, NORMALIZED_LAT_LNG_DISTANCE_STEP * 2)) + + def test_small_distance_same_spot(self): + sw = StepWalker(self.bot, 1, 0, 0) + self.assertEqual(sw.dLat, 0, 'dLat should be 0') + self.assertEqual(sw.dLng, 0, 'dLng should be 0') + + self.assertTrue(sw.step(), 'step should return True') + self.assertTrue(self.lat == self.bot.position[0]) + self.assertTrue(self.lng == self.bot.position[1]) + + def test_small_distance_small_step(self): + sw = StepWalker(self.bot, 1, 1e-5, 1e-5) + self.assertEqual(sw.dLat, 0) + self.assertEqual(sw.dLng, 0) + + @unittest.skip('This behavior is To Be Defined') + def test_big_distances(self): + # FIXME currently the StepWalker acts like it won't move if big distances gives as input + # see args below + # with self.assertRaises(RuntimeError): + sw = StepWalker(self.bot, 1, 10, 10) + sw.step() # equals True i.e act like the distance is too short for a step diff --git a/tests/tree_config_builder_test.py b/tests/tree_config_builder_test.py new file mode 100644 index 0000000000..cee1080280 --- /dev/null +++ b/tests/tree_config_builder_test.py @@ -0,0 +1,85 @@ +import unittest +import json +from pokemongo_bot import PokemonGoBot, ConfigException, TreeConfigBuilder +from pokemongo_bot.cell_workers import HandleSoftBan, CatchLuredPokemon + +def convert_from_json(str): + return json.loads(str) + +class TreeConfigBuilderTest(unittest.TestCase): + def setUp(self): + self.bot = {} + + def test_should_throw_on_no_type_key(self): + obj = convert_from_json("""[{ + "bad_key": "foo" + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + + self.assertRaisesRegexp( + ConfigException, + "No type found for given task", + builder.build) + + def test_should_throw_on_non_matching_type(self): + obj = convert_from_json("""[{ + "type": "foo" + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + + self.assertRaisesRegexp( + ConfigException, + "No worker named foo defined", + builder.build) + + def test_should_throw_on_wrong_evolve_task_name(self): + obj = convert_from_json("""[{ + "type": "EvolveAll" + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + + self.assertRaisesRegexp( + ConfigException, + "The EvolveAll task has been renamed to EvolvePokemon", + builder.build) + + def test_creating_worker(self): + obj = convert_from_json("""[{ + "type": "HandleSoftBan" + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + tree = builder.build() + + self.assertIsInstance(tree[0], HandleSoftBan) + self.assertIs(tree[0].bot, self.bot) + + def test_creating_two_workers(self): + obj = convert_from_json("""[{ + "type": "HandleSoftBan" + }, { + "type": "CatchLuredPokemon" + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + tree = builder.build() + + self.assertIsInstance(tree[0], HandleSoftBan) + self.assertIs(tree[0].bot, self.bot) + self.assertIsInstance(tree[1], CatchLuredPokemon) + self.assertIs(tree[1].bot, self.bot) + + def test_task_with_config(self): + obj = convert_from_json("""[{ + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + tree = builder.build() + self.assertTrue(tree[0].config.get('longer_eggs_first', False)) diff --git a/tests/update_title_stats_test.py b/tests/update_title_stats_test.py new file mode 100644 index 0000000000..699d736b7a --- /dev/null +++ b/tests/update_title_stats_test.py @@ -0,0 +1,131 @@ +import unittest +from datetime import datetime, timedelta +from mock import patch, MagicMock +from pokemongo_bot.cell_workers.update_title_stats import UpdateTitleStats +from tests import FakeBot + + +class UpdateTitleStatsTestCase(unittest.TestCase): + config = { + 'min_interval': 20, + 'stats': ['pokemon_evolved', 'pokemon_encountered', 'uptime', 'pokemon_caught', + 'stops_visited', 'km_walked', 'level', 'stardust_earned', 'level_completion', + 'xp_per_hour', 'pokeballs_thrown', 'highest_cp_pokemon', 'level_stats', + 'xp_earned', 'pokemon_unseen', 'most_perfect_pokemon', 'pokemon_stats', + 'pokemon_released'] + } + player_stats = { + 'level': 25, + 'prev_level_xp': 1250000, + 'next_level_xp': 1400000, + 'experience': 1337500 + } + + def setUp(self): + self.bot = FakeBot() + self.worker = UpdateTitleStats(self.bot, self.config) + + def mock_metrics(self): + self.bot.metrics = MagicMock() + self.bot.metrics.runtime.return_value = timedelta(hours=15, minutes=42, seconds=13) + self.bot.metrics.distance_travelled.return_value = 42.05 + self.bot.metrics.xp_per_hour.return_value = 1337.42 + self.bot.metrics.xp_earned.return_value = 424242 + self.bot.metrics.visits = {'latest': 250, 'start': 30} + self.bot.metrics.num_encounters.return_value = 130 + self.bot.metrics.num_captures.return_value = 120 + self.bot.metrics.releases = 30 + self.bot.metrics.num_evolutions.return_value = 12 + self.bot.metrics.num_new_mons.return_value = 3 + self.bot.metrics.num_throws.return_value = 145 + self.bot.metrics.earned_dust.return_value = 24069 + self.bot.metrics.highest_cp = {'desc': 'highest_cp'} + self.bot.metrics.most_perfect = {'desc': 'most_perfect'} + + def test_process_config(self): + self.assertEqual(self.worker.min_interval, self.config['min_interval']) + self.assertEqual(self.worker.displayed_stats, self.config['stats']) + + def test_should_display_no_next_update(self): + self.worker.next_update = None + + self.assertTrue(self.worker._should_display()) + + @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + def test_should_display_before_next_update(self, mock_datetime): + now = datetime.now() + mock_datetime.now.return_value = now - timedelta(seconds=20) + self.worker.next_update = now + + self.assertFalse(self.worker._should_display()) + + @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + def test_should_display_after_next_update(self, mock_datetime): + now = datetime.now() + mock_datetime.now.return_value = now + timedelta(seconds=20) + self.worker.next_update = now + + self.assertTrue(self.worker._should_display()) + + @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + def test_should_display_exactly_next_update(self, mock_datetime): + now = datetime.now() + mock_datetime.now.return_value = now + self.worker.next_update = now + + self.assertTrue(self.worker._should_display()) + + @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + def test_next_update_after_update_title(self, mock_datetime): + now = datetime.now() + mock_datetime.now.return_value = now + old_next_display_value = self.worker.next_update + self.worker._update_title('', 'linux2') + + self.assertNotEqual(self.worker.next_update, old_next_display_value) + self.assertEqual(self.worker.next_update, + now + timedelta(seconds=self.config['min_interval'])) + + @patch('pokemongo_bot.cell_workers.update_title_stats.stdout') + def test_update_title_linux_osx(self, mock_stdout): + self.worker._update_title('', 'linux') + + self.assertEqual(mock_stdout.write.call_count, 1) + + self.worker._update_title('', 'linux2') + + self.assertEqual(mock_stdout.write.call_count, 2) + + self.worker._update_title('', 'darwin') + + self.assertEqual(mock_stdout.write.call_count, 3) + + @unittest.skip("Didn't find a way to mock ctypes.windll.kernel32.SetConsoleTitleA") + def test_update_title_win32(self): + self.worker._update_title('', 'win32') + + def test_get_stats_title_player_stats_none(self): + title = self.worker._get_stats_title(None) + + self.assertEqual(title, '') + + def test_get_stats_no_displayed_stats(self): + self.worker.displayed_stats = [] + title = self.worker._get_stats_title(self.player_stats) + + self.assertEqual(title, '') + + def test_get_stats(self): + self.mock_metrics() + + title = self.worker._get_stats_title(self.player_stats) + expected = 'Evolved 12 pokemon | Encountered 130 pokemon | Uptime : 15:42:13 | ' \ + 'Caught 120 pokemon | Visited 220 stops | 42.05km walked | Level 25 | ' \ + 'Earned 24,069 Stardust | 87,500 / 150,000 XP (58%) | 1,337 XP/h | ' \ + 'Threw 145 pokeballs | Highest CP pokemon : highest_cp | ' \ + 'Level 25 (87,500 / 150,000, 58%) | +424,242 XP | ' \ + 'Encountered 3 new pokemon | Most perfect pokemon : most_perfect | ' \ + 'Encountered 130 pokemon, 120 caught, 30 released, 12 evolved, ' \ + '3 never seen before | Released 30 pokemon' + + self.assertEqual(title, expected) diff --git a/travis-pythoncheck.py b/travis-pythoncheck.py deleted file mode 100644 index 1095b04a12..0000000000 --- a/travis-pythoncheck.py +++ /dev/null @@ -1,48 +0,0 @@ -#! /usr/bin/env python -''' -Module that runs pylint on all python scripts found in a directory tree.. -''' - -import os -import re -import sys - -total = 0.0 -count = 0 - -def check(module): - ''' - apply pylint to the file specified if it is a *.py file - ''' - global total, count - - if module[-3:] == ".py": - - print "CHECKING ", module - pout = os.popen('pylint %s'% module, 'r') - for line in pout: - if re.match("E....:.", line): - print line - if "Your code has been rated at" in line: - print line - score = re.findall("\d.\d\d", line)[0] - total += float(score) - count += 1 - -if __name__ == "__main__": - try: - print sys.argv - BASE_DIRECTORY = sys.argv[1] - except IndexError: - print "no directory specified, defaulting to current working directory" - BASE_DIRECTORY = os.getcwd() - - print "looking for *.py scripts in subdirectories of ", BASE_DIRECTORY - for root, dirs, files in os.walk(BASE_DIRECTORY): - for name in files: - filepath = os.path.join(root, name) - check(filepath) - - print "==" * 50 - print "%d modules found"% count - print "AVERAGE SCORE = %.02f"% (total / count) \ No newline at end of file diff --git a/web b/web index dc742c598a..6ba5609c61 160000 --- a/web +++ b/web @@ -1 +1 @@ -Subproject commit dc742c598a2636337bd358dae8a558ef02159e8e +Subproject commit 6ba5609c6151507b5b832a74e471b6b7b1a182c9 diff --git a/ws_server.py b/ws_server.py new file mode 100755 index 0000000000..85d6bb3a01 --- /dev/null +++ b/ws_server.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# -*- coding: utf-8 - + +import argparse + +from pokemongo_bot.socketio_server.runner import SocketIoRunner + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument( + "--host", + help="Host for the websocket", + type=str, + default='localhost' + ) + parser.add_argument( + "--port", + help="Port for the websocket", + type=int, + default=4000 + ) + config = parser.parse_known_args()[0] + + s = SocketIoRunner("{}:{}".format(config.host, config.port)) + s._start_listening_blocking()