From c81666d751b9ca8cd338760fdf920194fabf4ee4 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sat, 6 Aug 2016 22:01:02 -0700 Subject: [PATCH 001/143] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5dac1a1497..ea02cdfe66 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -# PokemonGo-Bot +# PokemonGo-Bot (Working) PokemonGo bot is a project created by the [PokemonGoF](https://github.com/PokemonGoF) team. The project is currently setup in two main branches. `dev` and `master`. -## Where to get the dll/so ? +## Please submit PR to [Dev branch](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) + +## Where to get the dll/so ? A help channel is comming. You need grab them from internet. We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) From ad6c21aa1dc0902c62e4001f9a81ce1fcd8a9f59 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sun, 7 Aug 2016 00:01:55 -0700 Subject: [PATCH 002/143] Update README.md (#2685) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1e56579ca8..ba131d5c39 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ To ensure that all updates are documented - [@eggins](https://github.com/eggins) ## Credits - [tejado](https://github.com/tejado) many thanks for the API +- [U6 Group](http://pgoapi.com) for the U6 - [Mila432](https://github.com/Mila432/Pokemon_Go_API) for the login secrets - [elliottcarlson](https://github.com/elliottcarlson) for the Google Auth PR - [AeonLucid](https://github.com/AeonLucid/POGOProtos) for improved protos From fddf30dd64322219c945a1aba3483f603ce55bcc Mon Sep 17 00:00:00 2001 From: garyking Date: Sun, 7 Aug 2016 12:13:59 -0400 Subject: [PATCH 003/143] Update Readme (#2847) Fix typos --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ba131d5c39..f269d43e6d 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ The project is currently setup in two main branches. `dev` and `master`. ## Please submit PR to [Dev branch](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) -## Where to get the dll/so ? A help channel is comming. -You need grab them from internet. +## Where to get the DLL/SO? A help channel is coming. +You need to grab them from the Internet. We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) From e5b7eda6cd7054baadbd6440bf9b94d3d5cd88e3 Mon Sep 17 00:00:00 2001 From: mjmadsen Date: Sun, 7 Aug 2016 16:55:59 -0500 Subject: [PATCH 004/143] [dev] small fixes (#2912) * Fixed emit_event typo * Update CONTRIBUTORS.md * Changed initialization location for "bot" We use bot in main exception on 128 * Update pokecli.py --- CONTRIBUTORS.md | 1 + pokecli.py | 3 ++- pokemongo_bot/cell_workers/evolve_pokemon.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5a57ad6f8a..ee5aa7063d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -52,3 +52,4 @@ * Abraxas000 * lucasfevi * Moonlight-Angel + * mjmadsen diff --git a/pokecli.py b/pokecli.py index 38f33a035c..24a0f38ee3 100644 --- a/pokecli.py +++ b/pokecli.py @@ -52,8 +52,9 @@ logger.setLevel(logging.INFO) def main(): + bot = False + try: - bot = False logger.info('PokemonGO Bot v1.0') sys.stdout = codecs.getwriter('utf8')(sys.stdout) sys.stderr = codecs.getwriter('utf8')(sys.stderr) diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 74eb0abf79..c3903a685d 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -59,7 +59,7 @@ def _should_run(self): if result is 1: # Request success self.emit_event( 'used_lucky_egg', - formmated='Used lucky egg ({amount_left} left).', + formatted='Used lucky egg ({amount_left} left).', data={ 'amount_left': lucky_egg_count - 1 } From 5b3fe3d187b6b5fdfdede00f064bf7bcbd5a484d Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sun, 7 Aug 2016 15:18:09 -0700 Subject: [PATCH 005/143] Dev merge to master (#2939) * Adding plugin support (#2679) * Adding plugin support * Adding an empty __init__.py * Moving the base task to the project root (#2702) * Moving the base task to the project root * Moving the base class more * Changing the import again * Adding a heartbeat to the analytics (#2709) * Adding a heartbeat to the analytics * Heartbeat every 30 seconds, not every 5 * Don't double track clients * Fix 'local variable 'bot' referenced before assignment' * Providing an error if tasks don't work for the given api (#2732) * Fix for utf8 encoding when catching lured pokemon (#2720) * Fixing lure pokestop encoding * fixing lure encoding * Fix For catchable not being displayed on the web (#2719) * Fix For catchable not being displayed on the web * Update catch_visible_pokemon.py * Added encrypt.so compilation process to Dockerfile (#2695) * OS Detection for encrypt lib (#2768) Fix 32bit check, darwin and linux use the same file Make it a function Check if file exists, if not show error Define file_name first Fix return Check if file exists, if not show error Print info about paths Fix for 32/64bit detection * Fix Typo in unexpected_response_retry (#2531) fixes #2525 #2523 * Revert "changing license from MIT to GPLv3" This reverts commit 69fb64f2bf7c12e28c2bb6d2b636c6af55822448. * When the google analytics domain is blocked the bot crashed. (#2764) With a simple try / except this can be solved. Fix dirty catch all * Fixes #2698 - Prevents "Possibly searching too often" error after re-login. (#2771) * Fixes #2698 - Added api.activate_signature call to prevent issue after re-login. - Also replaced deprecated log call with event_manager emit to prevent exception being thrown. * Modified to use OS detected library path as per PR #2768 * Support loading plugins from .zip files (#2766) * Keep track of how many pokemon released (#2884) * Setting Library path to work with encrypt.so (#2899) Setting LD_LIBRARY_PATH on Dockerfile * :sparkles: Added login and username to available stats (#2494) Added a player_data property in PokemonGoBot to access player data from outside Added unit tests for login and username stats Added tests for call args when updating the window title Added a platform-specific test for window title updating on win32 platform * [dev] small fixes (#2912) * Fixed emit_event typo * Update CONTRIBUTORS.md * Changed initialization location for "bot" We use bot in main exception on 128 * Update pokecli.py --- CONTRIBUTORS.md | 2 + Dockerfile | 9 +- LICENSE | 676 +----------------- pokecli.py | 9 + pokemongo_bot/__init__.py | 52 +- pokemongo_bot/{cell_workers => }/base_task.py | 1 + pokemongo_bot/cell_workers/__init__.py | 3 +- .../cell_workers/catch_lured_pokemon.py | 8 +- .../cell_workers/catch_visible_pokemon.py | 8 +- .../cell_workers/collect_level_up_reward.py | 4 +- pokemongo_bot/cell_workers/evolve_pokemon.py | 5 +- pokemongo_bot/cell_workers/follow_cluster.py | 3 +- pokemongo_bot/cell_workers/follow_path.py | 4 +- pokemongo_bot/cell_workers/follow_spiral.py | 4 +- pokemongo_bot/cell_workers/handle_soft_ban.py | 4 +- pokemongo_bot/cell_workers/incubate_eggs.py | 4 +- pokemongo_bot/cell_workers/move_to_fort.py | 3 +- .../cell_workers/move_to_map_pokemon.py | 4 +- .../cell_workers/nickname_pokemon.py | 4 +- .../cell_workers/pokemon_catch_worker.py | 2 +- pokemongo_bot/cell_workers/recycle_items.py | 4 +- pokemongo_bot/cell_workers/sleep_schedule.py | 3 +- pokemongo_bot/cell_workers/spin_fort.py | 4 +- .../cell_workers/transfer_pokemon.py | 5 +- .../cell_workers/update_title_stats.py | 15 +- pokemongo_bot/health_record/bot_event.py | 48 +- pokemongo_bot/plugin_loader.py | 33 + pokemongo_bot/test/plugin_loader_test.py | 28 + pokemongo_bot/test/resources/__init__.py | 0 .../test/resources/plugin_fixture/__init__.py | 2 + .../resources/plugin_fixture/fake_task.py | 7 + .../plugin_fixture/unsupported_api_task.py | 7 + .../test/resources/plugin_fixture_test.zip | Bin 0 -> 3412 bytes pokemongo_bot/tree_config_builder.py | 33 +- tests/base_task_test.py | 2 +- tests/tree_config_builder_test.py | 55 +- tests/update_title_stats_test.py | 57 +- 37 files changed, 367 insertions(+), 745 deletions(-) rename pokemongo_bot/{cell_workers => }/base_task.py (96%) create mode 100644 pokemongo_bot/plugin_loader.py create mode 100644 pokemongo_bot/test/plugin_loader_test.py create mode 100644 pokemongo_bot/test/resources/__init__.py create mode 100644 pokemongo_bot/test/resources/plugin_fixture/__init__.py create mode 100644 pokemongo_bot/test/resources/plugin_fixture/fake_task.py create mode 100644 pokemongo_bot/test/resources/plugin_fixture/unsupported_api_task.py create mode 100644 pokemongo_bot/test/resources/plugin_fixture_test.zip diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index dfd8a24af2..ee5aa7063d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -51,3 +51,5 @@ * matheussampaio * Abraxas000 * lucasfevi + * Moonlight-Angel + * mjmadsen diff --git a/Dockerfile b/Dockerfile index 58c45cd02f..dce398c63e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,14 @@ RUN echo $timezone > /etc/timezone \ RUN apt-get update \ && apt-get install -y python-protobuf +RUN cd /tmp && wget "http://pgoapi.com/pgoencrypt.tar.gz" \ + && tar zxvf pgoencrypt.tar.gz \ + && cd pgoencrypt/src \ + && make \ + && cp libencrypt.so /usr/src/app/encrypt.so VOLUME ["/usr/src/app/web"] -ENTRYPOINT ["python", "pokecli.py"] \ No newline at end of file +ENV LD_LIBRARY_PATH /usr/src/app + +ENTRYPOINT ["python", "pokecli.py"] diff --git a/LICENSE b/LICENSE index 9cecc1d466..d71a77b230 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,8 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 +The MIT License (MIT) +Copyright (c) 2016 PokemonGoF Team - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - Preamble +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {one line to give the program's name and a brief idea of what it does.} - Copyright (C) {year} {name of author} - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - {project} Copyright (C) {year} {fullname} - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pokecli.py b/pokecli.py index 3d1b3736e7..24a0f38ee3 100644 --- a/pokecli.py +++ b/pokecli.py @@ -40,6 +40,7 @@ from pokemongo_bot import PokemonGoBot, TreeConfigBuilder from pokemongo_bot.health_record import BotEvent +from pokemongo_bot.plugin_loader import PluginLoader if sys.version_info >= (2, 7, 9): ssl._create_default_https_context = ssl._create_unverified_context @@ -51,6 +52,8 @@ logger.setLevel(logging.INFO) def main(): + bot = False + try: logger.info('PokemonGO Bot v1.0') sys.stdout = codecs.getwriter('utf8')(sys.stdout) @@ -73,6 +76,7 @@ def main(): tree = TreeConfigBuilder(bot, config.raw_tasks).build() bot.workers = tree bot.metrics.capture_stats() + bot.health_record = health_record bot.event_manager.emit( 'bot_start', @@ -384,6 +388,7 @@ def init_config(): config.release = load.get('release', {}) config.action_wait_max = load.get('action_wait_max', 4) config.action_wait_min = load.get('action_wait_min', 1) + config.plugins = load.get('plugins', []) config.raw_tasks = load.get('tasks', []) config.vips = load.get('vips', {}) @@ -439,6 +444,10 @@ def task_configuration_error(flag_name): parser.error("--catch_randomize_spin_factor is out of range! (should be 0 <= catch_randomize_spin_factor <= 1)") return None + plugin_loader = PluginLoader() + for plugin in config.plugins: + plugin_loader.load_path(plugin) + # create web dir if not exists try: os.makedirs(web_dir) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 1d318f473b..d0f034f110 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -15,6 +15,8 @@ from pgoapi.utilities import f2i, get_cell_ids import cell_workers +from base_task import BaseTask +from plugin_loader import PluginLoader from api_wrapper import ApiWrapper from cell_workers.utils import distance from event_manager import EventManager @@ -25,9 +27,9 @@ from pokemongo_bot.socketio_server.runner import SocketIoRunner from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl from worker_result import WorkerResult -from tree_config_builder import ConfigException, TreeConfigBuilder - - +from tree_config_builder import ConfigException, MismatchTaskApiVersion, TreeConfigBuilder +from sys import platform as _platform +import struct class PokemonGoBot(object): @property def position(self): @@ -37,6 +39,15 @@ def position(self): def position(self, position_tuple): self.api._position_lat, self.api._position_lng, self.api._position_alt = position_tuple + @property + def player_data(self): + """ + Returns the player data as received from the API. + :return: The player data. + :rtype: dict + """ + return self._player + def __init__(self, config): self.config = config self.fort_timeouts = dict() @@ -383,6 +394,7 @@ def _register_events(self): self.event_manager.register_event('unset_pokemon_nickname') def tick(self): + self.health_record.heartbeat() self.cell = self.get_meta_cell() self.tick_count += 1 @@ -544,11 +556,17 @@ def check_session(self, position): self.api._auth_provider._ticket_expire / 1000 - time.time() if remaining_time < 60: - self.logger.info("Session stale, re-logging in", 'yellow') + self.event_manager.emit( + 'api_error', + sender=self, + level='info', + formatted='Session stale, re-logging in.' + ) position = self.position self.api = ApiWrapper() self.position = position self.login() + self.api.activate_signature(self.get_encryption_lib()) @staticmethod def is_numeric(s): @@ -588,6 +606,29 @@ def login(self): formatted="Login successful." ) + def get_encryption_lib(self): + file_name = '' + if _platform == "linux" or _platform == "linux2" or _platform == "darwin": + file_name = 'encrypt.so' + elif _platform == "Windows" or _platform == "win32": + # Check if we are on 32 or 64 bit + if sys.maxsize > 2**32: + file_name = 'encrypt_64.dll' + else: + file_name = 'encrypt.dll' + + path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) + full_path = path + '/'+ file_name + + if not os.path.isfile(full_path): + self.logger.error(file_name + ' is not found! Please place it in the bots root directory.') + self.logger.info('Platform: '+ _platform + ' Bot root directory: '+ path) + sys.exit(1) + else: + self.logger.info('Found '+ file_name +'! Platform: ' + _platform + ' Bot root directory: ' + path) + + return full_path + def _setup_api(self): # instantiate pgoapi self.api = ApiWrapper() @@ -599,8 +640,7 @@ def _setup_api(self): # chain subrequests (methods) into one RPC call self._print_character_info() - - self.api.activate_signature("encrypt.so") + self.api.activate_signature(self.get_encryption_lib()) self.logger.info('') self.update_inventory() # send empty map_cells and then our position diff --git a/pokemongo_bot/cell_workers/base_task.py b/pokemongo_bot/base_task.py similarity index 96% rename from pokemongo_bot/cell_workers/base_task.py rename to pokemongo_bot/base_task.py index ac48b9a676..22bbedf4e8 100644 --- a/pokemongo_bot/cell_workers/base_task.py +++ b/pokemongo_bot/base_task.py @@ -2,6 +2,7 @@ class BaseTask(object): + TASK_API_VERSION = 1 def __init__(self, bot, config): self.bot = bot diff --git a/pokemongo_bot/cell_workers/__init__.py b/pokemongo_bot/cell_workers/__init__.py index bc6638d1fd..68d181947a 100644 --- a/pokemongo_bot/cell_workers/__init__.py +++ b/pokemongo_bot/cell_workers/__init__.py @@ -15,7 +15,6 @@ from follow_path import FollowPath from follow_spiral import FollowSpiral from collect_level_up_reward import CollectLevelUpReward -from base_task import BaseTask from follow_cluster import FollowCluster from sleep_schedule import SleepSchedule -from update_title_stats import UpdateTitleStats \ No newline at end of file +from update_title_stats import UpdateTitleStats diff --git a/pokemongo_bot/cell_workers/catch_lured_pokemon.py b/pokemongo_bot/cell_workers/catch_lured_pokemon.py index bf2d45bb4b..10a046dce9 100644 --- a/pokemongo_bot/cell_workers/catch_lured_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_lured_pokemon.py @@ -3,10 +3,12 @@ from pokemongo_bot.cell_workers.utils import fort_details from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class CatchLuredPokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def work(self): lured_pokemon = self.get_lured_pokemon() if lured_pokemon: @@ -22,7 +24,7 @@ def get_lured_pokemon(self): details = fort_details(self.bot, fort_id=fort['id'], latitude=fort['latitude'], longitude=fort['longitude']) - fort_name = details.get('name', 'Unknown').encode('utf8', 'replace') + fort_name = details.get('name', 'Unknown') encounter_id = fort.get('lure_info', {}).get('encounter_id', None) @@ -30,7 +32,7 @@ def get_lured_pokemon(self): result = { 'encounter_id': encounter_id, 'fort_id': fort['id'], - 'fort_name': fort_name, + 'fort_name': u"{}".format(fort_name), 'latitude': fort['latitude'], 'longitude': fort['longitude'] } diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py index 1103815a4c..1bfed225df 100644 --- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -1,11 +1,13 @@ import json -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker from utils import distance class CatchVisiblePokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def work(self): if 'catchable_pokemons' in self.bot.cell and len(self.bot.cell['catchable_pokemons']) > 0: # Sort all by distance from current pos- eventually this should @@ -14,9 +16,9 @@ def work(self): key= lambda x: distance(self.bot.position[0], self.bot.position[1], x['latitude'], x['longitude']) ) - + user_web_catchable = 'web/catchable-{}.json'.format(self.bot.config.username) for pokemon in self.bot.cell['catchable_pokemons']: - with open('user_web_catchable', 'w') as outfile: + with open(user_web_catchable, 'w') as outfile: json.dump(pokemon, outfile) self.emit_event( 'catchable_pokemon', diff --git a/pokemongo_bot/cell_workers/collect_level_up_reward.py b/pokemongo_bot/cell_workers/collect_level_up_reward.py index 304818fe2b..950f450660 100644 --- a/pokemongo_bot/cell_workers/collect_level_up_reward.py +++ b/pokemongo_bot/cell_workers/collect_level_up_reward.py @@ -1,7 +1,9 @@ -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class CollectLevelUpReward(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + current_level = 0 previous_level = 0 diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 911d6a1f67..c3903a685d 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -1,9 +1,10 @@ from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.item_list import Item -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class EvolvePokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 def initialize(self): self.api = self.bot.api @@ -58,7 +59,7 @@ def _should_run(self): if result is 1: # Request success self.emit_event( 'used_lucky_egg', - formmated='Used lucky egg ({amount_left} left).', + formatted='Used lucky egg ({amount_left} left).', data={ 'amount_left': lucky_egg_count - 1 } diff --git a/pokemongo_bot/cell_workers/follow_cluster.py b/pokemongo_bot/cell_workers/follow_cluster.py index 02d3880a7e..8448fcf742 100644 --- a/pokemongo_bot/cell_workers/follow_cluster.py +++ b/pokemongo_bot/cell_workers/follow_cluster.py @@ -1,9 +1,10 @@ from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.cell_workers.utils import distance from pokemongo_bot.cell_workers.utils import find_biggest_cluster -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class FollowCluster(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 def initialize(self): self.is_at_destination = False diff --git a/pokemongo_bot/cell_workers/follow_path.py b/pokemongo_bot/cell_workers/follow_path.py index 04eb817593..6e183ed1d7 100644 --- a/pokemongo_bot/cell_workers/follow_path.py +++ b/pokemongo_bot/cell_workers/follow_path.py @@ -3,7 +3,7 @@ import gpxpy import gpxpy.gpx import json -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.utils import distance, i2f, format_dist from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.step_walker import StepWalker @@ -11,6 +11,8 @@ class FollowPath(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): self.ptr = 0 self._process_config() diff --git a/pokemongo_bot/cell_workers/follow_spiral.py b/pokemongo_bot/cell_workers/follow_spiral.py index 28b548d1ca..f175369e45 100644 --- a/pokemongo_bot/cell_workers/follow_spiral.py +++ b/pokemongo_bot/cell_workers/follow_spiral.py @@ -5,9 +5,11 @@ from pokemongo_bot.cell_workers.utils import distance, format_dist from pokemongo_bot.step_walker import StepWalker -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class FollowSpiral(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): self.steplimit = self.config.get("diameter", 4) self.step_size = self.config.get("step_size", 70) diff --git a/pokemongo_bot/cell_workers/handle_soft_ban.py b/pokemongo_bot/cell_workers/handle_soft_ban.py index 4f9d416e83..8018b7c33e 100644 --- a/pokemongo_bot/cell_workers/handle_soft_ban.py +++ b/pokemongo_bot/cell_workers/handle_soft_ban.py @@ -3,13 +3,15 @@ from pgoapi.utilities import f2i from pokemongo_bot.constants import Constants -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers import MoveToFort from pokemongo_bot.cell_workers.utils import distance from pokemongo_bot.worker_result import WorkerResult class HandleSoftBan(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def work(self): if not self.should_run(): return diff --git a/pokemongo_bot/cell_workers/incubate_eggs.py b/pokemongo_bot/cell_workers/incubate_eggs.py index 9e21b0d280..5761090ea5 100644 --- a/pokemongo_bot/cell_workers/incubate_eggs.py +++ b/pokemongo_bot/cell_workers/incubate_eggs.py @@ -1,8 +1,10 @@ from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class IncubateEggs(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + last_km_walked = 0 def initialize(self): diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index f43d1641e6..e4b4187d20 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -4,11 +4,12 @@ from pokemongo_bot.constants import Constants from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from utils import distance, format_dist, fort_details class MoveToFort(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 def initialize(self): self.lure_distance = 0 diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index bce39e0143..08ff35f281 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -9,11 +9,13 @@ from pokemongo_bot.cell_workers.utils import distance, format_dist, format_time from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker class MoveToMapPokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): self.last_map_update = 0 self.pokemon_data = self.bot.pokemon_list diff --git a/pokemongo_bot/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py index 29df15ae4a..cda206ad20 100644 --- a/pokemongo_bot/cell_workers/nickname_pokemon.py +++ b/pokemongo_bot/cell_workers/nickname_pokemon.py @@ -1,7 +1,9 @@ from pokemongo_bot.human_behaviour import sleep -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class NicknamePokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): self.template = self.config.get('nickname_template','').lower().strip() if self.template == "{name}": diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index afa5578267..d676e9f0e2 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -3,7 +3,7 @@ import time from pokemongo_bot.human_behaviour import (normalized_reticle_size, sleep, spin_modifier) -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class PokemonCatchWorker(BaseTask): BAG_FULL = 'bag_full' diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index c28b2749b1..2c969913b0 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -1,9 +1,11 @@ import json import os -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.tree_config_builder import ConfigException class RecycleItems(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): self.item_filter = self.config.get('item_filter', {}) self._validate_item_filter() diff --git a/pokemongo_bot/cell_workers/sleep_schedule.py b/pokemongo_bot/cell_workers/sleep_schedule.py index daaf0b8f1e..5a7d617e23 100644 --- a/pokemongo_bot/cell_workers/sleep_schedule.py +++ b/pokemongo_bot/cell_workers/sleep_schedule.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta from time import sleep from random import uniform -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class SleepSchedule(BaseTask): @@ -27,6 +27,7 @@ class SleepSchedule(BaseTask): duration_random_offset: (HH:MM) random offset of duration of sleep for this example the possible duration is 5:00-6:00 """ + SUPPORTED_TASK_API_VERSION = 1 LOG_INTERVAL_SECONDS = 600 SCHEDULING_MARGIN = timedelta(minutes=10) # Skip if next sleep is RESCHEDULING_MARGIN from now diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 9572008241..e04a86dbc6 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -8,11 +8,13 @@ from pokemongo_bot.constants import Constants from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.worker_result import WorkerResult -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from utils import distance, format_time, fort_details class SpinFort(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def should_run(self): if not self.bot.has_space_for_loot(): self.emit_event( diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 70c5939c58..5c1d30fae7 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -1,10 +1,12 @@ import json from pokemongo_bot.human_behaviour import action_delay -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask class TransferPokemon(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + def work(self): pokemon_groups = self._release_pokemon_get_groups() for pokemon_id in pokemon_groups: @@ -185,6 +187,7 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): def release_pokemon(self, pokemon_name, cp, iv, pokemon_id): response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon_id) + self.bot.metrics.released_pokemon() self.emit_event( 'pokemon_release', formatted='Exchanged {pokemon} [CP {cp}] [IV {iv}] for candy.', diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index 911d2efd4d..43e55c260f 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -2,7 +2,7 @@ from sys import stdout, platform as _platform from datetime import datetime, timedelta -from pokemongo_bot.cell_workers.base_task import BaseTask +from pokemongo_bot.base_task import BaseTask from pokemongo_bot.worker_result import WorkerResult from pokemongo_bot.tree_config_builder import ConfigException @@ -18,11 +18,13 @@ class UpdateTitleStats(BaseTask): "type": "UpdateTitleStats", "config": { "min_interval": 10, - "stats": ["uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"] + "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"] } } Available stats : + - login : The account login (from the credentials). + - username : The trainer name (asked at first in-game connection). - uptime : The bot uptime. - km_walked : The kilometers walked since the bot started. - level : The current character's level. @@ -49,6 +51,7 @@ class UpdateTitleStats(BaseTask): stats : An array of stats to display and their display order (implicitly), see available stats above. """ + SUPPORTED_TASK_API_VERSION = 1 DEFAULT_MIN_INTERVAL = 10 DEFAULT_DISPLAYED_STATS = [] @@ -106,8 +109,7 @@ def _update_title(self, title, platform): :rtype: None :raise: RuntimeError: When the given platform isn't supported. """ - if platform == "linux" or platform == "linux2"\ - or platform == "cygwin": + if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) elif platform == "darwin": stdout.write("\033]0;{}\007".format(title)) @@ -144,6 +146,9 @@ def _get_stats_title(self, player_stats): metrics = self.bot.metrics metrics.capture_stats() runtime = metrics.runtime() + login = self.bot.config.username + player_data = self.bot.player_data + username = player_data.get('username', '?') distance_travelled = metrics.distance_travelled() current_level = int(player_stats.get('level', 0)) prev_level_xp = int(player_stats.get('prev_level_xp', 0)) @@ -171,6 +176,8 @@ def _get_stats_title(self, player_stats): # Create stats strings. available_stats = { + 'login': login, + 'username': username, 'uptime': 'Uptime : {}'.format(runtime), 'km_walked': '{:,.2f}km walked'.format(distance_travelled), 'level': 'Level {}'.format(current_level), diff --git a/pokemongo_bot/health_record/bot_event.py b/pokemongo_bot/health_record/bot_event.py index 986b5f3c70..f357a4e8e7 100644 --- a/pokemongo_bot/health_record/bot_event.py +++ b/pokemongo_bot/health_record/bot_event.py @@ -7,6 +7,7 @@ import os import uuid import requests +import time class BotEvent(object): def __init__(self, config): @@ -31,38 +32,51 @@ def __init__(self, config): context = {} ) + self.client_id = uuid.uuid4() + self.heartbeat_wait = 30 # seconds + self.last_heartbeat = time.time() + def capture_error(self): if self.config.health_record: self.client.captureException() def login_success(self): if self.config.health_record: - track_url('/loggedin') + self.last_heartbeat = time.time() + self.track_url('/loggedin') def login_failed(self): if self.config.health_record: - track_url('/login') + self.track_url('/login') def login_retry(self): if self.config.health_record: - track_url('/relogin') + self.track_url('/relogin') def logout(self): if self.config.health_record: - track_url('/logout') - + self.track_url('/logout') -def track_url(path): - data = { - 'v': '1', - 'tid': 'UA-81469507-1', - 'aip': '1', # Anonymize IPs - 'cid': uuid.uuid4(), - 't': 'pageview', - 'dp': path - } + def heartbeat(self): + if self.config.health_record: + current_time = time.time() + if current_time - self.heartbeat_wait > self.last_heartbeat: + self.last_heartbeat = current_time + self.track_url('/heartbeat') - response = requests.post( - 'http://www.google-analytics.com/collect', data=data) + def track_url(self, path): + data = { + 'v': '1', + 'tid': 'UA-81469507-1', + 'aip': '1', # Anonymize IPs + 'cid': self.client_id, + 't': 'pageview', + 'dp': path + } + try: + response = requests.post( + 'http://www.google-analytics.com/collect', data=data) - response.raise_for_status() + response.raise_for_status() + except requests.exceptions.HTTPError: + pass diff --git a/pokemongo_bot/plugin_loader.py b/pokemongo_bot/plugin_loader.py new file mode 100644 index 0000000000..3bded030b3 --- /dev/null +++ b/pokemongo_bot/plugin_loader.py @@ -0,0 +1,33 @@ +import os +import sys +import importlib + +class PluginLoader(object): + folder_cache = [] + + def _get_correct_path(self, path): + extension = os.path.splitext(path)[1] + + if extension == '.zip': + correct_path = path + else: + correct_path = os.path.dirname(path) + + return correct_path + + def load_path(self, path): + correct_path = self._get_correct_path(path) + + if correct_path not in self.folder_cache: + self.folder_cache.append(correct_path) + sys.path.append(correct_path) + + def remove_path(self, path): + correct_path = self._get_correct_path(path) + sys.path.remove(correct_path) + + def get_class(self, namespace_class): + [namespace, class_name] = namespace_class.split('.') + my_module = importlib.import_module(namespace) + return getattr(my_module, class_name) + diff --git a/pokemongo_bot/test/plugin_loader_test.py b/pokemongo_bot/test/plugin_loader_test.py new file mode 100644 index 0000000000..4d4d5ca952 --- /dev/null +++ b/pokemongo_bot/test/plugin_loader_test.py @@ -0,0 +1,28 @@ +import imp +import sys +import pkgutil +import importlib +import unittest +import os +from datetime import timedelta, datetime +from mock import patch, MagicMock +from pokemongo_bot.plugin_loader import PluginLoader +from pokemongo_bot.test.resources.plugin_fixture import FakeTask + +class PluginLoaderTest(unittest.TestCase): + def setUp(self): + self.plugin_loader = PluginLoader() + + def test_load_namespace_class(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') + self.plugin_loader.load_path(package_path) + loaded_class = self.plugin_loader.get_class('plugin_fixture.FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + self.plugin_loader.remove_path(package_path) + + def test_load_zip(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture_test.zip') + self.plugin_loader.load_path(package_path) + loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + self.plugin_loader.remove_path(package_path) diff --git a/pokemongo_bot/test/resources/__init__.py b/pokemongo_bot/test/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pokemongo_bot/test/resources/plugin_fixture/__init__.py b/pokemongo_bot/test/resources/plugin_fixture/__init__.py new file mode 100644 index 0000000000..57caf83dce --- /dev/null +++ b/pokemongo_bot/test/resources/plugin_fixture/__init__.py @@ -0,0 +1,2 @@ +from fake_task import FakeTask +from unsupported_api_task import UnsupportedApiTask diff --git a/pokemongo_bot/test/resources/plugin_fixture/fake_task.py b/pokemongo_bot/test/resources/plugin_fixture/fake_task.py new file mode 100644 index 0000000000..ff729adee4 --- /dev/null +++ b/pokemongo_bot/test/resources/plugin_fixture/fake_task.py @@ -0,0 +1,7 @@ +from pokemongo_bot.base_task import BaseTask + +class FakeTask(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + + def work(self): + return 'FakeTask' diff --git a/pokemongo_bot/test/resources/plugin_fixture/unsupported_api_task.py b/pokemongo_bot/test/resources/plugin_fixture/unsupported_api_task.py new file mode 100644 index 0000000000..871e38f82d --- /dev/null +++ b/pokemongo_bot/test/resources/plugin_fixture/unsupported_api_task.py @@ -0,0 +1,7 @@ +from pokemongo_bot.base_task import BaseTask + +class UnsupportedApiTask(BaseTask): + SUPPORTED_TASK_API_VERSION = 2 + + def work(): + return 2 diff --git a/pokemongo_bot/test/resources/plugin_fixture_test.zip b/pokemongo_bot/test/resources/plugin_fixture_test.zip new file mode 100644 index 0000000000000000000000000000000000000000..335d95e5226056902bb02114fa26a2b3dad61ca1 GIT binary patch literal 3412 zcmb7`dpuNmAIFdDjNGOgOqm!(61n7>TxMKy88Xx`+>91!# zS^64&kyMF7CH9A-CuBq$WQv$0!|~I2>_j&EdT`Jc&7K!`;P7HO7Xlsz4+J>05>PZz z6fh^oyqq5~{BlZwVvtDAIBOSouQ?5&#-VFL!{u*-G+ew*&^VT&mLJEq)lk|bfs>(- zoki#|to+?!_A|~-HQQQSNU7tZn3mANA z!?Sw^pf*Rr>#I@rq4>2?7GM+-DIhq2K_Y3>BOt3B2i0o{mN zd(<`VsMFc*N*7tf0%rCHV20Z7{n0L*dNk1Sgdc8eLOp?%Nx^OPlAS9`um8M=30ROj zUr|f_k6FDa%0g8O?*cIu?0w`%M>5QwIaJeKyE$S0P zGoyWhQ+*qg+b)c5E9@4^_EvgRf5lJl$PZ_@j>ZN{lvY(Kxf zYxMD#eN5_0N?F?k5if`&3lH!s@{g0rvJH1*Ag1&1368JUQjn9p_K+INV|i+RB#pX z4WlkIQNyI-HF3Hq)_b!@w646?vX|ta#56j{?S(4kr_Z6H9X3bKia(gV`)ND~lfurN zG)T!6cdW(P<#2zHA>Aj~SG>X=JUD7TggWwDaDS+d%pq}(`hbtl#e8mAMgEVsho4Mc z9aFkN_j~>Um8dM{xkvvwO+@tUQ{9|SOUZXIs5N~{q(daw8h1ros0gqHZ)^YXMWw5& zV+|IO-O9K)QT*fo& zy0aoe29iHTb{_Bia?Fujx&K9a#k58|(o$Rv`v~J#< zXn$Ac5$u4{Wcf!%zW;?XJ0yd3vgKBgLArnQ$ezl&hlj`WtvmuF@|)x9C+^rvs=eVP zb;KLHj9JYk)`M#wr|%L^5S`o9cGNNl!;L)P#YvhPtf6lS zKQV~w%y6jY6VdCO?4J3V!vB0!b$@+I4(8s)B$4mCI|{r0V{u0@`tHq`@XqL#nftrA))*>! z#(3z=|Bj%!Z4vhJiIPbD!q)_U{deA{&b5BJ#jeaaeac_wNjAD!Dzy?pyFPr;V}Gxq zLUPp8te`HKwn_2kt@{1eZ621gHfL`S;y(#PU23MFHbSOMS<#eQ|E|k_`16rl_o6>B z|Aj~45Tl=CI0f=+i=`d%JJZ!@=5Pw4q>V?nQjg8X+{jC#y$W%mkL;Cq zJ-zosbmHZRY%TQ@^-mi*-&zPk(^D&EMt%c&^xd3Cg@olq1Z6ELaK3W~b_@r7FTd5p z7n2tO57EAE^t>Tt6Y!Omd(uh(jc2ciFQ%}0?DA1Wf(JW`a{=303?@Xcc?1+hRzmPr zHj9hrMF?tmA=@l%m^WE$ worker.SUPPORTED_TASK_API_VERSION: + error_string = 'Is there a new version of this task?' + + if error_string != '': + raise MismatchTaskApiVersion( + 'Task {} only works with task api version {}, you are currently running version {}. {}' + .format( + task_type, + worker.SUPPORTED_TASK_API_VERSION, + BaseTask.TASK_API_VERSION, + error_string + ) + ) + instance = worker(self.bot, task_config) workers.append(instance) diff --git a/tests/base_task_test.py b/tests/base_task_test.py index 16684d900c..ee259f80dc 100644 --- a/tests/base_task_test.py +++ b/tests/base_task_test.py @@ -1,6 +1,6 @@ import unittest import json -from pokemongo_bot.cell_workers import BaseTask +from pokemongo_bot.base_task import BaseTask class FakeTask(BaseTask): def initialize(self): diff --git a/tests/tree_config_builder_test.py b/tests/tree_config_builder_test.py index cee1080280..cd8bed22e0 100644 --- a/tests/tree_config_builder_test.py +++ b/tests/tree_config_builder_test.py @@ -1,7 +1,9 @@ import unittest import json -from pokemongo_bot import PokemonGoBot, ConfigException, TreeConfigBuilder +import os +from pokemongo_bot import PokemonGoBot, ConfigException, MismatchTaskApiVersion, TreeConfigBuilder, PluginLoader, BaseTask from pokemongo_bot.cell_workers import HandleSoftBan, CatchLuredPokemon +from pokemongo_bot.test.resources.plugin_fixture import FakeTask, UnsupportedApiTask def convert_from_json(str): return json.loads(str) @@ -83,3 +85,54 @@ def test_task_with_config(self): builder = TreeConfigBuilder(self.bot, obj) tree = builder.build() self.assertTrue(tree[0].config.get('longer_eggs_first', False)) + + def test_load_plugin_task(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') + plugin_loader = PluginLoader() + plugin_loader.load_path(package_path) + + obj = convert_from_json("""[{ + "type": "plugin_fixture.FakeTask" + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + tree = builder.build() + result = tree[0].work() + self.assertEqual(result, 'FakeTask') + + def setupUnsupportedBuilder(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'pokemongo_bot', 'test', 'resources', 'plugin_fixture') + plugin_loader = PluginLoader() + plugin_loader.load_path(package_path) + + obj = convert_from_json("""[{ + "type": "plugin_fixture.UnsupportedApiTask" + }]""") + + return TreeConfigBuilder(self.bot, obj) + + def test_task_version_too_high(self): + builder = self.setupUnsupportedBuilder() + + previous_version = BaseTask.TASK_API_VERSION + BaseTask.TASK_API_VERSION = 1 + + self.assertRaisesRegexp( + MismatchTaskApiVersion, + "Task plugin_fixture.UnsupportedApiTask only works with task api version 2, you are currently running version 1. Do you need to update the bot?", + builder.build) + + BaseTask.TASK_API_VERSION = previous_version + + def test_task_version_too_low(self): + builder = self.setupUnsupportedBuilder() + + previous_version = BaseTask.TASK_API_VERSION + BaseTask.TASK_API_VERSION = 3 + + self.assertRaisesRegexp( + MismatchTaskApiVersion, + "Task plugin_fixture.UnsupportedApiTask only works with task api version 2, you are currently running version 3. Is there a new version of this task?", + builder.build) + + BaseTask.TASK_API_VERSION = previous_version diff --git a/tests/update_title_stats_test.py b/tests/update_title_stats_test.py index 699d736b7a..ba480f0151 100644 --- a/tests/update_title_stats_test.py +++ b/tests/update_title_stats_test.py @@ -1,6 +1,7 @@ import unittest +from sys import platform as _platform from datetime import datetime, timedelta -from mock import patch, MagicMock +from mock import call, patch, MagicMock from pokemongo_bot.cell_workers.update_title_stats import UpdateTitleStats from tests import FakeBot @@ -8,11 +9,11 @@ class UpdateTitleStatsTestCase(unittest.TestCase): config = { 'min_interval': 20, - 'stats': ['pokemon_evolved', 'pokemon_encountered', 'uptime', 'pokemon_caught', - 'stops_visited', 'km_walked', 'level', 'stardust_earned', 'level_completion', - 'xp_per_hour', 'pokeballs_thrown', 'highest_cp_pokemon', 'level_stats', - 'xp_earned', 'pokemon_unseen', 'most_perfect_pokemon', 'pokemon_stats', - 'pokemon_released'] + 'stats': ['login', 'username', 'pokemon_evolved', 'pokemon_encountered', 'uptime', + 'pokemon_caught', 'stops_visited', 'km_walked', 'level', 'stardust_earned', + 'level_completion', 'xp_per_hour', 'pokeballs_thrown', 'highest_cp_pokemon', + 'level_stats', 'xp_earned', 'pokemon_unseen', 'most_perfect_pokemon', + 'pokemon_stats', 'pokemon_released'] } player_stats = { 'level': 25, @@ -23,6 +24,8 @@ class UpdateTitleStatsTestCase(unittest.TestCase): def setUp(self): self.bot = FakeBot() + self.bot._player = {'username': 'Username'} + self.bot.config.username = 'Login' self.worker = UpdateTitleStats(self.bot, self.config) def mock_metrics(self): @@ -87,22 +90,37 @@ def test_next_update_after_update_title(self, mock_datetime): now + timedelta(seconds=self.config['min_interval'])) @patch('pokemongo_bot.cell_workers.update_title_stats.stdout') - def test_update_title_linux_osx(self, mock_stdout): - self.worker._update_title('', 'linux') + def test_update_title_linux_cygwin(self, mock_stdout): + self.worker._update_title('new title linux', 'linux') self.assertEqual(mock_stdout.write.call_count, 1) + self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title linux\x07')) - self.worker._update_title('', 'linux2') + self.worker._update_title('new title linux2', 'linux2') self.assertEqual(mock_stdout.write.call_count, 2) + self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title linux2\x07')) - self.worker._update_title('', 'darwin') + self.worker._update_title('new title cygwin', 'cygwin') self.assertEqual(mock_stdout.write.call_count, 3) + self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title cygwin\x07')) + + @patch('pokemongo_bot.cell_workers.update_title_stats.stdout') + def test_update_title_darwin(self, mock_stdout): + self.worker._update_title('new title darwin', 'darwin') + + self.assertEqual(mock_stdout.write.call_count, 1) + self.assertEqual(mock_stdout.write.call_args, call('\033]0;new title darwin\007')) + + @unittest.skipUnless(_platform.startswith("win"), "requires Windows") + @patch('pokemongo_bot.cell_workers.update_title_stats.ctypes') + def test_update_title_win32(self, mock_ctypes): + self.worker._update_title('new title win32', 'win32') - @unittest.skip("Didn't find a way to mock ctypes.windll.kernel32.SetConsoleTitleA") - def test_update_title_win32(self): - self.worker._update_title('', 'win32') + self.assertEqual(mock_ctypes.windll.kernel32.SetConsoleTitleA.call_count, 1) + self.assertEqual(mock_ctypes.windll.kernel32.SetConsoleTitleA.call_args, + call('new title win32')) def test_get_stats_title_player_stats_none(self): title = self.worker._get_stats_title(None) @@ -119,12 +137,13 @@ def test_get_stats(self): self.mock_metrics() title = self.worker._get_stats_title(self.player_stats) - expected = 'Evolved 12 pokemon | Encountered 130 pokemon | Uptime : 15:42:13 | ' \ - 'Caught 120 pokemon | Visited 220 stops | 42.05km walked | Level 25 | ' \ - 'Earned 24,069 Stardust | 87,500 / 150,000 XP (58%) | 1,337 XP/h | ' \ - 'Threw 145 pokeballs | Highest CP pokemon : highest_cp | ' \ - 'Level 25 (87,500 / 150,000, 58%) | +424,242 XP | ' \ - 'Encountered 3 new pokemon | Most perfect pokemon : most_perfect | ' \ + expected = 'Login | Username | Evolved 12 pokemon | Encountered 130 pokemon | ' \ + 'Uptime : 15:42:13 | Caught 120 pokemon | Visited 220 stops | ' \ + '42.05km walked | Level 25 | Earned 24,069 Stardust | ' \ + '87,500 / 150,000 XP (58%) | 1,337 XP/h | Threw 145 pokeballs | ' \ + 'Highest CP pokemon : highest_cp | Level 25 (87,500 / 150,000, 58%) | ' \ + '+424,242 XP | Encountered 3 new pokemon | ' \ + 'Most perfect pokemon : most_perfect | ' \ 'Encountered 130 pokemon, 120 caught, 30 released, 12 evolved, ' \ '3 never seen before | Released 30 pokemon' From dee28d9fb1a973b95019bc07fb1e206872bd986d Mon Sep 17 00:00:00 2001 From: Eli White Date: Sun, 7 Aug 2016 15:58:16 -0700 Subject: [PATCH 006/143] Rename load_path to load_plugin (#2947) --- pokecli.py | 2 +- pokemongo_bot/plugin_loader.py | 2 +- pokemongo_bot/test/plugin_loader_test.py | 4 ++-- tests/tree_config_builder_test.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pokecli.py b/pokecli.py index 24a0f38ee3..a77cf378c9 100644 --- a/pokecli.py +++ b/pokecli.py @@ -446,7 +446,7 @@ def task_configuration_error(flag_name): plugin_loader = PluginLoader() for plugin in config.plugins: - plugin_loader.load_path(plugin) + plugin_loader.load_plugin(plugin) # create web dir if not exists try: diff --git a/pokemongo_bot/plugin_loader.py b/pokemongo_bot/plugin_loader.py index 3bded030b3..9362197bc8 100644 --- a/pokemongo_bot/plugin_loader.py +++ b/pokemongo_bot/plugin_loader.py @@ -15,7 +15,7 @@ def _get_correct_path(self, path): return correct_path - def load_path(self, path): + def load_plugin(self, path): correct_path = self._get_correct_path(path) if correct_path not in self.folder_cache: diff --git a/pokemongo_bot/test/plugin_loader_test.py b/pokemongo_bot/test/plugin_loader_test.py index 4d4d5ca952..38a566b35c 100644 --- a/pokemongo_bot/test/plugin_loader_test.py +++ b/pokemongo_bot/test/plugin_loader_test.py @@ -15,14 +15,14 @@ def setUp(self): def test_load_namespace_class(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') - self.plugin_loader.load_path(package_path) + self.plugin_loader.load_plugin(package_path) loaded_class = self.plugin_loader.get_class('plugin_fixture.FakeTask') self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') self.plugin_loader.remove_path(package_path) def test_load_zip(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture_test.zip') - self.plugin_loader.load_path(package_path) + self.plugin_loader.load_plugin(package_path) loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') self.plugin_loader.remove_path(package_path) diff --git a/tests/tree_config_builder_test.py b/tests/tree_config_builder_test.py index cd8bed22e0..1992c8187c 100644 --- a/tests/tree_config_builder_test.py +++ b/tests/tree_config_builder_test.py @@ -89,7 +89,7 @@ def test_task_with_config(self): def test_load_plugin_task(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') plugin_loader = PluginLoader() - plugin_loader.load_path(package_path) + plugin_loader.load_plugin(package_path) obj = convert_from_json("""[{ "type": "plugin_fixture.FakeTask" @@ -103,7 +103,7 @@ def test_load_plugin_task(self): def setupUnsupportedBuilder(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'pokemongo_bot', 'test', 'resources', 'plugin_fixture') plugin_loader = PluginLoader() - plugin_loader.load_path(package_path) + plugin_loader.load_plugin(package_path) obj = convert_from_json("""[{ "type": "plugin_fixture.UnsupportedApiTask" From 0855dac7c34cc46b69af9bd4e708e6d172498b31 Mon Sep 17 00:00:00 2001 From: Eli White Date: Sun, 7 Aug 2016 18:08:21 -0700 Subject: [PATCH 007/143] Adding some logic for pulling plugins from github (#2967) --- pokemongo_bot/plugin_loader.py | 57 +++++++++++++++++++++++- pokemongo_bot/plugins/.keep | 1 + pokemongo_bot/test/plugin_loader_test.py | 26 ++++++++++- 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 pokemongo_bot/plugins/.keep diff --git a/pokemongo_bot/plugin_loader.py b/pokemongo_bot/plugin_loader.py index 9362197bc8..8666b53254 100644 --- a/pokemongo_bot/plugin_loader.py +++ b/pokemongo_bot/plugin_loader.py @@ -1,6 +1,8 @@ import os import sys import importlib +import re +import requests class PluginLoader(object): folder_cache = [] @@ -15,8 +17,8 @@ def _get_correct_path(self, path): return correct_path - def load_plugin(self, path): - correct_path = self._get_correct_path(path) + def load_plugin(self, plugin): + correct_path = self._get_correct_path(plugin) if correct_path not in self.folder_cache: self.folder_cache.append(correct_path) @@ -31,3 +33,54 @@ def get_class(self, namespace_class): my_module = importlib.import_module(namespace) return getattr(my_module, class_name) +class GithubPlugin(object): + def __init__(self, plugin_name): + self.plugin_name = plugin_name + self.plugin_parts = self.get_github_parts() + + def is_valid_plugin(self): + return self.plugin_parts is not None + + def get_github_parts(self): + groups = re.match('(.*)\/(.*)#(.*)', self.plugin_name) + + if groups is None: + return None + + parts = {} + parts['user'] = groups.group(1) + parts['repo'] = groups.group(2) + parts['sha'] = groups.group(3) + + return parts + + def get_local_destination(self): + parts = self.plugin_parts + if parts is None: + raise Exception('Not a valid github plugin') + + file_name = '{}_{}_{}.zip'.format(parts['user'], parts['repo'], parts['sha']) + full_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'plugins', file_name) + return full_path + + def get_github_download_url(self): + parts = self.plugin_parts + if parts is None: + raise Exception('Not a valid github plugin') + + github_url = 'https://github.com/{}/{}/archive/{}.zip'.format(parts['user'], parts['repo'], parts['sha']) + return github_url + + def download(self): + url = self.get_github_download_url() + dest = self.get_local_destination() + + r = requests.get(url, stream=True) + r.raise_for_status() + + with open(dest, 'wb') as f: + for chunk in r.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) + r.close() + return dest diff --git a/pokemongo_bot/plugins/.keep b/pokemongo_bot/plugins/.keep new file mode 100644 index 0000000000..5d848b8301 --- /dev/null +++ b/pokemongo_bot/plugins/.keep @@ -0,0 +1 @@ +keep this so we can install plugins into this folder diff --git a/pokemongo_bot/test/plugin_loader_test.py b/pokemongo_bot/test/plugin_loader_test.py index 38a566b35c..ecf50888e6 100644 --- a/pokemongo_bot/test/plugin_loader_test.py +++ b/pokemongo_bot/test/plugin_loader_test.py @@ -6,7 +6,7 @@ import os from datetime import timedelta, datetime from mock import patch, MagicMock -from pokemongo_bot.plugin_loader import PluginLoader +from pokemongo_bot.plugin_loader import PluginLoader, GithubPlugin from pokemongo_bot.test.resources.plugin_fixture import FakeTask class PluginLoaderTest(unittest.TestCase): @@ -26,3 +26,27 @@ def test_load_zip(self): loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') self.plugin_loader.remove_path(package_path) + + def test_get_github_parts_for_valid_github(self): + github_plugin = GithubPlugin('org/repo#sha') + self.assertTrue(github_plugin.is_valid_plugin()) + self.assertEqual(github_plugin.plugin_parts['user'], 'org') + self.assertEqual(github_plugin.plugin_parts['repo'], 'repo') + self.assertEqual(github_plugin.plugin_parts['sha'], 'sha') + + def test_get_github_parts_for_invalid_github(self): + self.assertFalse(GithubPlugin('org/repo').is_valid_plugin()) + self.assertFalse(GithubPlugin('foo').is_valid_plugin()) + self.assertFalse(GithubPlugin('/Users/foo/bar.zip').is_valid_plugin()) + + def test_get_local_destination(self): + github_plugin = GithubPlugin('org/repo#sha') + path = github_plugin.get_local_destination() + expected = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins', 'org_repo_sha.zip')) + self.assertEqual(path, expected) + + def test_get_github_download_url(self): + github_plugin = GithubPlugin('org/repo#sha') + url = github_plugin.get_github_download_url() + expected = 'https://github.com/org/repo/archive/sha.zip' + self.assertEqual(url, expected) From a1733b92b9cb4bd5a864ec506e7709d14d56674b Mon Sep 17 00:00:00 2001 From: mhdasding Date: Mon, 8 Aug 2016 05:16:25 +0200 Subject: [PATCH 008/143] flush after title update (#2977) --- pokemongo_bot/cell_workers/update_title_stats.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index 43e55c260f..acbfaa7fe4 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -111,8 +111,10 @@ def _update_title(self, title, platform): """ if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) + stdout.flush() elif platform == "darwin": stdout.write("\033]0;{}\007".format(title)) + stdout.flush() elif platform == "win32": ctypes.windll.kernel32.SetConsoleTitleA(title) else: From e66c50951c1c0e44a21253d124c82a1bc78ae449 Mon Sep 17 00:00:00 2001 From: rbignon Date: Mon, 8 Aug 2016 05:17:05 +0200 Subject: [PATCH 009/143] correctly re-raise exception to keep backtrace (#2944) --- pokecli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokecli.py b/pokecli.py index a77cf378c9..e00389e06e 100644 --- a/pokecli.py +++ b/pokecli.py @@ -53,7 +53,7 @@ def main(): bot = False - + try: logger.info('PokemonGO Bot v1.0') sys.stdout = codecs.getwriter('utf8')(sys.stdout) @@ -130,7 +130,7 @@ def main(): if bot: report_summary(bot) - raise e + raise def report_summary(bot): if bot.metrics.start_time is None: From bdcf2519d63555ac32246439fd17672896ee64e4 Mon Sep 17 00:00:00 2001 From: Chris Le Date: Sun, 7 Aug 2016 20:17:50 -0700 Subject: [PATCH 010/143] Update MoveToMapPokemon to use events instead of logger. (#2913) --- .gitignore | 5 + pokemongo_bot/__init__.py | 29 +++ .../cell_workers/move_to_map_pokemon.py | 231 +++++++++++++++--- pokemongo_bot/health_record/bot_event.py | 2 +- 4 files changed, 233 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index a12509c322..70ab34b250 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,9 @@ var/ .pydevproject .settings/ +# Cloud9 Users +.c9/ + # Installer logs pip-log.txt pip-delete-this-directory.txt @@ -86,6 +89,8 @@ celerybeat-schedule # virtualenv venv/ ENV/ +local/ +share/ # Spyder project settings .spyderproject diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index d0f034f110..1378e5bdb9 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -393,6 +393,35 @@ def _register_events(self): ) self.event_manager.register_event('unset_pokemon_nickname') + # Move To map pokemon + self.event_manager.register_event( + 'move_to_map_pokemon_fail', + parameters=('message',) + ) + self.event_manager.register_event( + 'move_to_map_pokemon_updated_map', + parameters=('lat', 'lon') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_teleport_to', + parameters=('poke_name', 'poke_dist', 'poke_lat', 'poke_lon', + 'disappears_in') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_encounter', + parameters=('poke_name', 'poke_dist', 'poke_lat', 'poke_lon', + 'disappears_in') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_move_towards', + parameters=('poke_name', 'poke_dist', 'poke_lat', 'poke_lon', + 'disappears_in') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_teleport_back', + parameters=('last_lat', 'last_lon') + ) + def tick(self): self.health_record.heartbeat() self.cell = self.get_meta_cell() diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 08ff35f281..975a9b5a36 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -1,11 +1,59 @@ # -*- coding: utf-8 -*- +""" +Moves a trainer to a Pokemon. + +Events: + move_to_map_pokemon_fail + When the worker fails. + Returns: + message: Failure message. + + move_to_map_pokemon_updated_map + When worker updates the PokemonGo-Map. + Returns: + lat: Latitude + lon: Longitude + + move_to_map_pokemon_teleport_to + When trainer is teleported to a Pokemon. + Returns: + poke_name: Pokemon's name + poke_dist: Distance from the trainer + poke_lat: Latitude of the Pokemon + poke_lon: Longitude of the Pokemon + disappears_in: Number of seconds before the Pokemon disappears + + move_to_map_pokemon_encounter + When a trainer encounters a Pokemon by teleporting or walking. + Returns: + poke_name: Pokemon's name + poke_dist: Distance from the trainer + poke_lat: Latitude of the Pokemon + poke_lon: Longitude of the Pokemon + disappears_in: Number of seconds before the Pokemon disappears + + move_to_map_pokemon_move_towards + When a trainer moves toward a Pokemon. + Returns: + poke_name: Pokemon's name + poke_dist: Distance from the trainer + poke_lat: Latitude of the Pokemon + poke_lon: Longitude of the Pokemon + disappears_in: Number of seconds before the Pokemon disappears + + move_to_map_pokemon_teleport_back + When a trainer teleports back to thier previous location. + Returns: + last_lat: Trainer's last known latitude + last_lon: Trainer's last known longitude + +""" import os import time import json import base64 import requests -from pokemongo_bot import logger from pokemongo_bot.cell_workers.utils import distance, format_dist, format_time from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult @@ -13,7 +61,20 @@ from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker +# Update the map if more than N meters away from the center. (AND'd with +# UPDATE_MAP_MIN_TIME_MINUTES) +UPDATE_MAP_MIN_DISTANCE_METERS = 500 + +# Update the map if it hasn't been updated in n seconds. (AND'd with +# UPDATE_MAP_MIN_DISTANCE_METERS) +UPDATE_MAP_MIN_TIME_SEC = 120 + +# Number of seconds to sleep between teleporting to a snipped Pokemon. +SNIPE_SLEEP_SEC = 2 + + class MoveToMapPokemon(BaseTask): + """Task for moving a trainer to a Pokemon.""" SUPPORTED_TASK_API_VERSION = 1 def initialize(self): @@ -32,13 +93,15 @@ def get_pokemon_from_map(self): try: req = requests.get('{}/raw_data?gyms=false&scanned=false'.format(self.config['address'])) except requests.exceptions.ConnectionError: - logger.log('Could not reach PokemonGo-Map Server', 'red') + self._emit_failure('Could not get Pokemon data from PokemonGo-Map: ' + '{}. Is it running?'.format( + self.config['address'])) return [] try: raw_data = req.json() except ValueError: - logger.log('Map data was not valid', 'red') + self._emit_failure('Map data was not valid') return [] pokemon_list = [] @@ -48,7 +111,7 @@ def get_pokemon_from_map(self): try: pokemon['encounter_id'] = long(base64.b64decode(pokemon['encounter_id'])) except TypeError: - log.logger('base64 error: {}'.format(pokemon['encounter_id']), 'red') + self._emit_failure('base64 error: {}'.format(pokemon['encounter_id'])) continue pokemon['spawn_point_id'] = pokemon['spawnpoint_id'] pokemon['disappear_time'] = int(pokemon['disappear_time'] / 1000) @@ -100,14 +163,17 @@ def update_map_location(self): try: req = requests.get('{}/loc'.format(self.config['address'])) except requests.exceptions.ConnectionError: - logger.log('Could not reach PokemonGo-Map Server', 'red') + self._emit_failure('Could not update trainer location ' + 'PokemonGo-Map: {}. Is it running?'.format( + self.config['address'])) return try: loc_json = req.json() except ValueError: - return log.logger('Map location data was not valid', 'red') - + err = 'Map location data was not valid' + self._emit_failure(err) + return log.logger(err, 'red') dist = distance( self.bot.position[0], @@ -118,32 +184,38 @@ def update_map_location(self): # update map when 500m away from center and last update longer than 2 minutes away now = int(time.time()) - if dist > 500 and now - self.last_map_update > 2 * 60: - requests.post('{}/next_loc?lat={}&lon={}'.format(self.config['address'], self.bot.position[0], self.bot.position[1])) - logger.log('Updated PokemonGo-Map position') + if (dist > UPDATE_MAP_MIN_DISTANCE_METERS and + now - self.last_map_update > UPDATE_MAP_MIN_TIME_SEC): + requests.post( + '{}/next_loc?lat={}&lon={}'.format(self.config['address'], + self.bot.position[0], + self.bot.position[1])) + self.emit_event( + 'move_to_map_pokemon_updated_map', + formatted='Updated PokemonGo-Map to {lat}, {lon}', + data={ + 'lat': self.bot.position[0], + 'lon': self.bot.position[1] + } + ) self.last_map_update = now def snipe(self, pokemon): - last_position = self.bot.position[0:2] - + """Snipe a Pokemon by teleporting. + + Args: + pokemon: Pokemon to snipe. + """ self.bot.heartbeat() - - logger.log('Teleporting to {} ({})'.format(pokemon['name'], format_dist(pokemon['dist'], self.unit)), 'green') - self.bot.api.set_position(pokemon['latitude'], pokemon['longitude'], 0) - - logger.log('Encounter pokemon', 'green') + self._teleport_to(pokemon) catch_worker = PokemonCatchWorker(pokemon, self.bot) api_encounter_response = catch_worker.create_encounter_api_call() - - time.sleep(2) - logger.log('Teleporting back to previous location..', 'green') - self.bot.api.set_position(last_position[0], last_position[1], 0) - time.sleep(2) + time.sleep(SNIPE_SLEEP_SEC) + self._teleport_back() + time.sleep(SNIPE_SLEEP_SEC) self.bot.heartbeat() - catch_worker.work(api_encounter_response) self.add_caught(pokemon) - return WorkerResult.SUCCESS def dump_caught_pokemon(self): @@ -182,18 +254,111 @@ def work(self): if self.config['snipe']: return self.snipe(pokemon) + step_walker = self._move_to(pokemon) + if not step_walker.step(): + return WorkerResult.RUNNING + self._encountered(pokemon) + self.add_caught(pokemon) + return WorkerResult.SUCCESS + + def _emit_failure(self, msg): + """Emits failure to event log. + + Args: + msg: Message to emit + """ + self.emit_event( + 'move_to_map_pokemon_fail', + formatted='Failure! {message}', + data={'message': msg} + ) + + def _emit_log(self, msg): + """Emits log to event log. + + Args: + msg: Message to emit + """ + self.emit_event( + 'move_to_map_pokemon', + formatted='{message}', + data={'message': msg} + ) + + def _pokemon_event_data(self, pokemon): + """Generates parameters used for the Bot's event manager. + + Args: + pokemon: Pokemon object + + Returns: + Dictionary with Pokemon's info. + """ + now = int(time.time()) + return { + 'poke_name': pokemon['name'], + 'poke_dist': (format_dist(pokemon['dist'], self.unit)), + 'poke_lat': pokemon['latitude'], + 'poke_lon': pokemon['longitude'], + 'disappears_in': (format_time(pokemon['disappear_time'] - now)) + } + + def _teleport_to(self, pokemon): + """Teleports trainer to a Pokemon. + + Args: + pokemon: Pokemon to teleport to. + """ + self.emit_event( + 'move_to_map_pokemon_teleport_to', + formatted='Teleporting to {poke_name}. ({poke_dist})', + data=self._pokemon_event_data(pokemon) + ) + self.bot.api.set_position(pokemon['latitude'], pokemon['longitude'], 0) + self._encountered(pokemon) + + def _encountered(self, pokemon): + """Emit event when trainer encounters a Pokemon. + + Args: + pokemon: Pokemon encountered. + """ + self.emit_event( + 'move_to_map_pokemon_encounter', + formatted='Encountered Pokemon: {poke_name}', + data=self._pokemon_event_data(pokemon) + ) + + def _teleport_back(self): + """Teleports trainer back to their last position.""" + last_position = self.bot.position[0:2] + self.emit_event( + 'move_to_map_pokemon_teleport_back', + formatted=('Teleporting back to previous location ({last_lat}, ' + '{last_long})'), + data={'last_lat': last_position[0], 'last_lon': last_position[1]} + ) + self.bot.api.set_position(last_position[0], last_position[1], 0) + + def _move_to(self, pokemon): + """Moves trainer towards a Pokemon. + + Args: + pokemon: Pokemon to move to. + + Returns: + StepWalker + """ now = int(time.time()) - logger.log('Moving towards {}, {} left ({})'.format(pokemon['name'], format_dist(pokemon['dist'], self.unit), format_time(pokemon['disappear_time'] - now))) - step_walker = StepWalker( + self.emit_event( + 'move_to_map_pokemon_move_towards', + formatted=('Moving towards {poke_name}, {poke_dist}, left (' + '{disappears_in})'), + data=self._pokemon_event_data(pokemon) + ) + return StepWalker( self.bot, self.bot.config.walk, pokemon['latitude'], pokemon['longitude'] ) - - if not step_walker.step(): - return WorkerResult.RUNNING - - logger.log('Arrived at {}'.format(pokemon['name'])) - self.add_caught(pokemon) - return WorkerResult.SUCCESS diff --git a/pokemongo_bot/health_record/bot_event.py b/pokemongo_bot/health_record/bot_event.py index f357a4e8e7..55a726049d 100644 --- a/pokemongo_bot/health_record/bot_event.py +++ b/pokemongo_bot/health_record/bot_event.py @@ -16,7 +16,7 @@ def __init__(self, config): # UniversalAnalytics can be reviewed here: # https://github.com/analytics-pros/universal-analytics-python if self.config.health_record: - self.logger.info('Health check is enabled. For more logrmation:') + self.logger.info('Health check is enabled. For more information:') self.logger.info('https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev#analytics') self.client = Client( dsn='https://8abac56480f34b998813d831de262514:196ae1d8dced41099f8253ea2c8fe8e6@app.getsentry.com/90254', From 95902d6ceac7b26da9bade1ce193d066f67c6c04 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 7 Aug 2016 23:59:50 -0400 Subject: [PATCH 011/143] Config/encrypt.so (#2964) * Add config option for libencrypt.so * Correctly set the config value and check for the file in said dir --- configs/config.json.example | 1 + pokecli.py | 1 + pokemongo_bot/__init__.py | 14 ++++++++------ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/configs/config.json.example b/configs/config.json.example index 20ef72e34e..ec46c15eb9 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/pokecli.py b/pokecli.py index e00389e06e..7cc5b5ebf1 100644 --- a/pokecli.py +++ b/pokecli.py @@ -384,6 +384,7 @@ def init_config(): if not config.password and 'password' not in load: config.password = getpass("Password: ") + config.encrypt_location = load.get('encrypt_location','') config.catch = load.get('catch', {}) config.release = load.get('release', {}) config.action_wait_max = load.get('action_wait_max', 4) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 1378e5bdb9..78498e37b0 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -636,7 +636,6 @@ def login(self): ) def get_encryption_lib(self): - file_name = '' if _platform == "linux" or _platform == "linux2" or _platform == "darwin": file_name = 'encrypt.so' elif _platform == "Windows" or _platform == "win32": @@ -646,15 +645,18 @@ def get_encryption_lib(self): else: file_name = 'encrypt.dll' - path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) - full_path = path + '/'+ file_name + if self.config.encrypt_location == '': + path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) + else: + path = self.config.encrypt_location + full_path = path + '/'+ file_name if not os.path.isfile(full_path): - self.logger.error(file_name + ' is not found! Please place it in the bots root directory.') - self.logger.info('Platform: '+ _platform + ' Bot root directory: '+ path) + self.logger.error(file_name + ' is not found! Please place it in the bots root directory or set libencrypt_location in config.') + self.logger.info('Platform: '+ _platform + ' Encrypt.so directory: '+ path) sys.exit(1) else: - self.logger.info('Found '+ file_name +'! Platform: ' + _platform + ' Bot root directory: ' + path) + self.logger.info('Found '+ file_name +'! Platform: ' + _platform + ' Encrypt.so directory: ' + path) return full_path From 41ed10cc9fd0e9a96d76a160095087617f32e849 Mon Sep 17 00:00:00 2001 From: middleagedman Date: Mon, 8 Aug 2016 00:27:13 -0400 Subject: [PATCH 012/143] Fixed mispelling for "formatted" variable (#2984) --- pokecli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokecli.py b/pokecli.py index 7cc5b5ebf1..d428026276 100644 --- a/pokecli.py +++ b/pokecli.py @@ -104,7 +104,7 @@ def main(): 'api_error', sender=bot, level='info', - formmated='Log logged in, reconnecting in {:s}'.format(wait_time) + formatted='Log logged in, reconnecting in {:s}'.format(wait_time) ) time.sleep(wait_time) except ServerBusyOrOfflineException: From 563f898f61233ff86d38d83b68d8399c1f63051b Mon Sep 17 00:00:00 2001 From: Eli White Date: Sun, 7 Aug 2016 21:32:53 -0700 Subject: [PATCH 013/143] Loading plugins from Github (#2992) * Checking github plugin file existence * Loading plugins from github --- pokemongo_bot/plugin_loader.py | 14 ++++++++- pokemongo_bot/test/plugin_loader_test.py | 37 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/pokemongo_bot/plugin_loader.py b/pokemongo_bot/plugin_loader.py index 8666b53254..7a838bb209 100644 --- a/pokemongo_bot/plugin_loader.py +++ b/pokemongo_bot/plugin_loader.py @@ -18,7 +18,14 @@ def _get_correct_path(self, path): return correct_path def load_plugin(self, plugin): - correct_path = self._get_correct_path(plugin) + github_plugin = GithubPlugin(plugin) + if github_plugin.is_valid_plugin(): + if not github_plugin.is_already_downloaded(): + github_plugin.download() + + correct_path = github_plugin.get_local_destination() + else: + correct_path = self._get_correct_path(plugin) if correct_path not in self.folder_cache: self.folder_cache.append(correct_path) @@ -27,6 +34,7 @@ def load_plugin(self, plugin): def remove_path(self, path): correct_path = self._get_correct_path(path) sys.path.remove(correct_path) + self.folder_cache.remove(correct_path) def get_class(self, namespace_class): [namespace, class_name] = namespace_class.split('.') @@ -63,6 +71,10 @@ def get_local_destination(self): full_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'plugins', file_name) return full_path + def is_already_downloaded(self): + file_path = self.get_local_destination() + return os.path.isfile(file_path) + def get_github_download_url(self): parts = self.plugin_parts if parts is None: diff --git a/pokemongo_bot/test/plugin_loader_test.py b/pokemongo_bot/test/plugin_loader_test.py index ecf50888e6..0b0f7da9d1 100644 --- a/pokemongo_bot/test/plugin_loader_test.py +++ b/pokemongo_bot/test/plugin_loader_test.py @@ -4,6 +4,8 @@ import importlib import unittest import os +import shutil +import mock from datetime import timedelta, datetime from mock import patch, MagicMock from pokemongo_bot.plugin_loader import PluginLoader, GithubPlugin @@ -27,6 +29,30 @@ def test_load_zip(self): self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') self.plugin_loader.remove_path(package_path) + def copy_zip(self): + zip_fixture = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture_test.zip') + dest_path = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins', 'org_repo_sha.zip')) + shutil.copyfile(zip_fixture, dest_path) + return dest_path + + def test_load_github_already_downloaded(self): + dest_path = self.copy_zip() + self.plugin_loader.load_plugin('org/repo#sha') + loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + self.plugin_loader.remove_path(dest_path) + os.remove(dest_path) + + @mock.patch.object(GithubPlugin, 'download', copy_zip) + def test_load_github_not_downloaded(self): + self.plugin_loader.load_plugin('org/repo#sha') + loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + dest_path = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins', 'org_repo_sha.zip')) + self.plugin_loader.remove_path(dest_path) + os.remove(dest_path) + +class GithubPluginTest(unittest.TestCase): def test_get_github_parts_for_valid_github(self): github_plugin = GithubPlugin('org/repo#sha') self.assertTrue(github_plugin.is_valid_plugin()) @@ -50,3 +76,14 @@ def test_get_github_download_url(self): url = github_plugin.get_github_download_url() expected = 'https://github.com/org/repo/archive/sha.zip' self.assertEqual(url, expected) + + def test_is_already_downloaded_not_downloaded(self): + github_plugin = GithubPlugin('org/repo#sha') + self.assertFalse(github_plugin.is_already_downloaded()) + + def test_is_already_downloaded_downloaded(self): + github_plugin = GithubPlugin('org/repo#sha') + dest = github_plugin.get_local_destination() + open(dest, 'a').close() + self.assertTrue(github_plugin.is_already_downloaded()) + os.remove(dest) From 229381c318c2f230257dc56f51cd0fd5213ee2d2 Mon Sep 17 00:00:00 2001 From: raulgbcr Date: Mon, 8 Aug 2016 14:12:25 +0200 Subject: [PATCH 014/143] Fixed #3000 (#3003) Fixed syntax error on "move_to_map_pokemon.py" that makes the client crash when using this feature. --- pokemongo_bot/cell_workers/move_to_map_pokemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 975a9b5a36..ec3c8bf89c 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -335,7 +335,7 @@ def _teleport_back(self): self.emit_event( 'move_to_map_pokemon_teleport_back', formatted=('Teleporting back to previous location ({last_lat}, ' - '{last_long})'), + '{last_lon})'), data={'last_lat': last_position[0], 'last_lon': last_position[1]} ) self.bot.api.set_position(last_position[0], last_position[1], 0) From 1a18b9f3b34f1f75bfae9c29ea2985aa3d59653b Mon Sep 17 00:00:00 2001 From: Jaap Moolenaar Date: Mon, 8 Aug 2016 14:13:07 +0200 Subject: [PATCH 015/143] Added MaxPotion inventory count to summary. (#3015) Short Description: The Max Potion count was missing from the inventory summary. Was #2456 --- pokemongo_bot/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 78498e37b0..788e20e721 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -747,7 +747,8 @@ def _print_character_info(self): self.logger.info( 'Potion: ' + str(items_stock[101]) + ' | SuperPotion: ' + str(items_stock[102]) + - ' | HyperPotion: ' + str(items_stock[103])) + ' | HyperPotion: ' + str(items_stock[103]) + + ' | MaxPotion: ' + str(items_stock[104])) self.logger.info( 'Incense: ' + str(items_stock[401]) + From 4faf9624519e1ea9513e3f3c887435fb81c56ab0 Mon Sep 17 00:00:00 2001 From: Arthur Caranta Date: Mon, 8 Aug 2016 14:13:32 +0200 Subject: [PATCH 016/143] Added cleanup of download and files for encrypt.so after they are no longer needed (#3011) --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index dce398c63e..456ae4fb5a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,8 @@ RUN cd /tmp && wget "http://pgoapi.com/pgoencrypt.tar.gz" \ && cd pgoencrypt/src \ && make \ && cp libencrypt.so /usr/src/app/encrypt.so + && cd /tmp + && rm -rf /tmp/pgoencrypt* VOLUME ["/usr/src/app/web"] From 7cc524ed80a918170efcd782829ed6a94e2c4674 Mon Sep 17 00:00:00 2001 From: Jeremy Bi Date: Mon, 8 Aug 2016 20:16:38 +0800 Subject: [PATCH 017/143] Fix bot not returning back after telepoting (#3014) * Fix typo: last_long -> last_lon * Whitespace cleanup * Fix bug introduced by #3037: bot not returning back --- .../cell_workers/move_to_map_pokemon.py | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index ec3c8bf89c..9ab6bbb7a2 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -7,13 +7,13 @@ When the worker fails. Returns: message: Failure message. - + move_to_map_pokemon_updated_map When worker updates the PokemonGo-Map. Returns: lat: Latitude lon: Longitude - + move_to_map_pokemon_teleport_to When trainer is teleported to a Pokemon. Returns: @@ -22,7 +22,7 @@ poke_lat: Latitude of the Pokemon poke_lon: Longitude of the Pokemon disappears_in: Number of seconds before the Pokemon disappears - + move_to_map_pokemon_encounter When a trainer encounters a Pokemon by teleporting or walking. Returns: @@ -31,7 +31,7 @@ poke_lat: Latitude of the Pokemon poke_lon: Longitude of the Pokemon disappears_in: Number of seconds before the Pokemon disappears - + move_to_map_pokemon_move_towards When a trainer moves toward a Pokemon. Returns: @@ -40,7 +40,7 @@ poke_lat: Latitude of the Pokemon poke_lon: Longitude of the Pokemon disappears_in: Number of seconds before the Pokemon disappears - + move_to_map_pokemon_teleport_back When a trainer teleports back to thier previous location. Returns: @@ -184,11 +184,11 @@ def update_map_location(self): # update map when 500m away from center and last update longer than 2 minutes away now = int(time.time()) - if (dist > UPDATE_MAP_MIN_DISTANCE_METERS and + if (dist > UPDATE_MAP_MIN_DISTANCE_METERS and now - self.last_map_update > UPDATE_MAP_MIN_TIME_SEC): requests.post( - '{}/next_loc?lat={}&lon={}'.format(self.config['address'], - self.bot.position[0], + '{}/next_loc?lat={}&lon={}'.format(self.config['address'], + self.bot.position[0], self.bot.position[1])) self.emit_event( 'move_to_map_pokemon_updated_map', @@ -202,16 +202,18 @@ def update_map_location(self): def snipe(self, pokemon): """Snipe a Pokemon by teleporting. - + Args: pokemon: Pokemon to snipe. """ + last_position = self.bot.position[0:2] self.bot.heartbeat() self._teleport_to(pokemon) catch_worker = PokemonCatchWorker(pokemon, self.bot) api_encounter_response = catch_worker.create_encounter_api_call() time.sleep(SNIPE_SLEEP_SEC) - self._teleport_back() + self._teleport_back(last_position) + self.bot.api.set_position(last_position[0], last_position[1], 0) time.sleep(SNIPE_SLEEP_SEC) self.bot.heartbeat() catch_worker.work(api_encounter_response) @@ -263,34 +265,34 @@ def work(self): def _emit_failure(self, msg): """Emits failure to event log. - + Args: msg: Message to emit """ self.emit_event( - 'move_to_map_pokemon_fail', + 'move_to_map_pokemon_fail', formatted='Failure! {message}', data={'message': msg} ) - + def _emit_log(self, msg): """Emits log to event log. - + Args: msg: Message to emit """ self.emit_event( - 'move_to_map_pokemon', + 'move_to_map_pokemon', formatted='{message}', data={'message': msg} ) - + def _pokemon_event_data(self, pokemon): """Generates parameters used for the Bot's event manager. - + Args: pokemon: Pokemon object - + Returns: Dictionary with Pokemon's info. """ @@ -298,14 +300,14 @@ def _pokemon_event_data(self, pokemon): return { 'poke_name': pokemon['name'], 'poke_dist': (format_dist(pokemon['dist'], self.unit)), - 'poke_lat': pokemon['latitude'], + 'poke_lat': pokemon['latitude'], 'poke_lon': pokemon['longitude'], 'disappears_in': (format_time(pokemon['disappear_time'] - now)) } - + def _teleport_to(self, pokemon): """Teleports trainer to a Pokemon. - + Args: pokemon: Pokemon to teleport to. """ @@ -316,7 +318,7 @@ def _teleport_to(self, pokemon): ) self.bot.api.set_position(pokemon['latitude'], pokemon['longitude'], 0) self._encountered(pokemon) - + def _encountered(self, pokemon): """Emit event when trainer encounters a Pokemon. @@ -329,23 +331,21 @@ def _encountered(self, pokemon): data=self._pokemon_event_data(pokemon) ) - def _teleport_back(self): - """Teleports trainer back to their last position.""" - last_position = self.bot.position[0:2] + def _teleport_back(self, last_position): + """Teleports trainer back to their last position.""" self.emit_event( 'move_to_map_pokemon_teleport_back', formatted=('Teleporting back to previous location ({last_lat}, ' '{last_lon})'), data={'last_lat': last_position[0], 'last_lon': last_position[1]} ) - self.bot.api.set_position(last_position[0], last_position[1], 0) - + def _move_to(self, pokemon): """Moves trainer towards a Pokemon. - + Args: pokemon: Pokemon to move to. - + Returns: StepWalker """ From 283c17e5dcb58267d895629cfa968aa65cae975d Mon Sep 17 00:00:00 2001 From: Nikos Filippakis Date: Mon, 8 Aug 2016 17:24:24 +0200 Subject: [PATCH 018/143] Fix Dockerfile installation (#3057) --- CONTRIBUTORS.md | 1 + Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ee5aa7063d..6dceaf0918 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -53,3 +53,4 @@ * lucasfevi * Moonlight-Angel * mjmadsen + * nikofil diff --git a/Dockerfile b/Dockerfile index 456ae4fb5a..f98d5d6942 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,8 +11,8 @@ RUN cd /tmp && wget "http://pgoapi.com/pgoencrypt.tar.gz" \ && tar zxvf pgoencrypt.tar.gz \ && cd pgoencrypt/src \ && make \ - && cp libencrypt.so /usr/src/app/encrypt.so - && cd /tmp + && cp libencrypt.so /usr/src/app/encrypt.so \ + && cd /tmp \ && rm -rf /tmp/pgoencrypt* VOLUME ["/usr/src/app/web"] From 351ea76b62a2708cae72d59977b42c6f3f204250 Mon Sep 17 00:00:00 2001 From: cmezh Date: Mon, 8 Aug 2016 22:42:17 +0700 Subject: [PATCH 019/143] Fix for #3045 (#3055) --- pokecli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokecli.py b/pokecli.py index d428026276..65d9923798 100644 --- a/pokecli.py +++ b/pokecli.py @@ -104,7 +104,7 @@ def main(): 'api_error', sender=bot, level='info', - formatted='Log logged in, reconnecting in {:s}'.format(wait_time) + formatted='Log logged in, reconnecting in {:d}'.format(wait_time) ) time.sleep(wait_time) except ServerBusyOrOfflineException: From ae0ae815089bf1e3b17497b476d524a0468949e2 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Mon, 8 Aug 2016 09:21:44 -0700 Subject: [PATCH 020/143] Update README.md (#3090) --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index f269d43e6d..171ee4f355 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,8 @@ The project is currently setup in two main branches. `dev` and `master`. ## Please submit PR to [Dev branch](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) -## Where to get the DLL/SO? A help channel is coming. -You need to grab them from the Internet. - We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) +You can count on the community in #help channel. ## Table of Contents - [Features](#features) From 20aeb90bc0712d4f023c311844f4c1174061d600 Mon Sep 17 00:00:00 2001 From: mjmadsen Date: Mon, 8 Aug 2016 11:22:19 -0500 Subject: [PATCH 021/143] Added request to check configuration (#3089) --- .github/ISSUE_TEMPLATE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 9976991cf9..a1d7168e75 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,5 @@ +Please check configuration at http://jsonlint.com/ before posting an issue. + ### Expected Behavior From ff380cd0e07f225ad1ae2c739bea9f9f80e2f8ce Mon Sep 17 00:00:00 2001 From: middleagedman Date: Mon, 8 Aug 2016 12:49:26 -0400 Subject: [PATCH 022/143] Fixed Dockerfile - missing \ on command lines (#3096) * Fixed mispelling for "formatted" variable * Docker commands missing trailing \ From a5e91315ed82581a901fbf5019a3708e5e7a471b Mon Sep 17 00:00:00 2001 From: Ajurna Date: Mon, 8 Aug 2016 19:34:31 +0100 Subject: [PATCH 023/143] Fix for FileIO slowing bot performance.This puts the map writing into a thread and makes sure it only executes once. (#3100) --- pokemongo_bot/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 788e20e721..67c7f9a6b0 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -9,6 +9,8 @@ import re import sys import time +import Queue +import threading from geopy.geocoders import GoogleV3 from pgoapi import PGoApi @@ -69,6 +71,11 @@ def __init__(self, config): # Make our own copy of the workers for this instance self.workers = [] + # Theading setup for file writing + self.web_update_queue = Queue.Queue(maxsize=1) + self.web_update_thread = threading.Thread(target=self.update_web_location_worker) + self.web_update_thread.start() + def start(self): self._setup_event_system() self._setup_logging() @@ -976,7 +983,15 @@ def heartbeat(self): request.get_player() request.check_awarded_badges() request.call() - self.update_web_location() # updates every tick + try: + self.web_update_queue.put_nowait(True) # do this outside of thread every tick + except Queue.Full: + pass + + def update_web_location_worker(self): + while True: + self.web_update_queue.get() + self.update_web_location() def get_inventory_count(self, what): response_dict = self.get_inventory() From d8546d7bfa660e827c903582da0c7ac714db8406 Mon Sep 17 00:00:00 2001 From: Tushar Saini Date: Mon, 8 Aug 2016 13:39:33 -0500 Subject: [PATCH 024/143] Change word usage: "fled" to "escaped" (#3118) "fled" is confusing to lot of people and is easily confused with pokemon vanishing. "escaped" is a better term. --- pokemongo_bot/cell_workers/pokemon_catch_worker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index d676e9f0e2..d5118b391b 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -324,8 +324,8 @@ def work(self, response_dict=None): 'CATCH_POKEMON']['status'] if status is 2: self.emit_event( - 'pokemon_fled', - formatted="{pokemon} fled.", + 'pokemon_escaped', + formatted="{pokemon} escaped.", data={'pokemon': pokemon_name} ) sleep(2) From 0b3aa4f5fcb6d353e062019c2b482b6a5f2631fc Mon Sep 17 00:00:00 2001 From: HKLCF Date: Tue, 9 Aug 2016 02:40:41 +0800 Subject: [PATCH 025/143] Update the example config file (#3120) * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Rename path.example.json to path.json.example --- configs/config.json.cluster.example | 1 + configs/config.json.map.example | 1 + configs/config.json.path.example | 1 + configs/config.json.pokemon.example | 1 + configs/{path.example.json => path.json.example} | 0 5 files changed, 4 insertions(+) rename configs/{path.example.json => path.json.example} (100%) diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index 8d0d8f854f..b32eb4f668 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.map.example b/configs/config.json.map.example index e665d4c6da..1079c999f9 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.path.example b/configs/config.json.path.example index afd1e3afeb..94a9fdba07 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 7cad1ac066..1d428a6ae7 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/path.example.json b/configs/path.json.example similarity index 100% rename from configs/path.example.json rename to configs/path.json.example From d0f60a221a8af7eaf7b41ffeeb756a636032534e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kay=20G=C3=BCrcan?= Date: Mon, 8 Aug 2016 21:44:49 +0300 Subject: [PATCH 026/143] typo: logrmation -> information (#2601) Fix a typo. I assume that it was "information" initially, but became "logrmation" when someone used replace all functionality to replace all infos with logs. But I might be totally wrong at this point, idk. Just didn't like the word and wanted to fix that typo. From f648be31c15d0143bccc0531571b2d00fd88ec4d Mon Sep 17 00:00:00 2001 From: pmquan Date: Mon, 8 Aug 2016 12:08:50 -0700 Subject: [PATCH 027/143] Change fled to escaped (#3129) Fix an issue after PR #3118 --- pokemongo_bot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 67c7f9a6b0..8e9a28ce3a 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -256,7 +256,7 @@ def _register_events(self): ) ) self.event_manager.register_event( - 'pokemon_fled', + 'pokemon_escaped', parameters=('pokemon',) ) self.event_manager.register_event( From 47ab81f5e1e34a36a33cf9c46949b5e51ab6e412 Mon Sep 17 00:00:00 2001 From: Chris Wild Date: Mon, 8 Aug 2016 21:07:09 +0100 Subject: [PATCH 028/143] When JSON parsing fails, give a rough indication of why (#3137) * When JSON parsing fails, give a rough indication of why * Use the official package instead of SHA1 commit --- pokecli.py | 26 ++++++++++++++++++++++---- requirements.txt | 1 + 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/pokecli.py b/pokecli.py index 65d9923798..55afa55399 100644 --- a/pokecli.py +++ b/pokecli.py @@ -42,6 +42,12 @@ from pokemongo_bot.health_record import BotEvent from pokemongo_bot.plugin_loader import PluginLoader +try: + from demjson import jsonlint +except ImportError: + # Run `pip install -r requirements.txt` to fix this + jsonlint = None + if sys.version_info >= (2, 7, 9): ssl._create_default_https_context = ssl._create_unverified_context @@ -162,16 +168,28 @@ def init_config(): # If config file exists, load variables from json load = {} + def _json_loader(filename): + try: + with open(filename, 'rb') as data: + load.update(json.load(data)) + except ValueError: + if jsonlint: + with open(filename, 'rb') as data: + lint = jsonlint() + rc = lint.main(['-v', filename]) + + logger.critical('Error with configuration file') + sys.exit(-1) + # Select a config file code parser.add_argument("-cf", "--config", help="Config File to use") config_arg = parser.parse_known_args() and parser.parse_known_args()[0].config or None + if config_arg and os.path.isfile(config_arg): - with open(config_arg) as data: - load.update(json.load(data)) + _json_loader(config_arg) elif os.path.isfile(config_file): logger.info('No config argument specified, checking for /configs/config.json') - with open(config_file) as data: - load.update(json.load(data)) + _json_loader(config_file) else: logger.info('Error: No /configs/config.json or specified config') diff --git a/requirements.txt b/requirements.txt index aabf40937d..f6a22a0233 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,3 +21,4 @@ gpxpy==1.1.1 mock==2.0.0 timeout-decorator==0.3.2 raven==5.23.0 +demjson==2.2.4 From c8a33bca367a7bd77b499e711ccc4f12142e3e5b Mon Sep 17 00:00:00 2001 From: Eli White Date: Mon, 8 Aug 2016 13:39:24 -0700 Subject: [PATCH 029/143] Handle Github Download Zip Format (#3108) * Checking github plugin file existence * Loading plugins from github * Starting install code for github plugins * Updating GithubPlugin to support extracting folders * Handling github zip formats by extracting to the correct location --- pokemongo_bot/plugin_loader.py | 64 +++++++++-- pokemongo_bot/test/plugin_loader_test.py | 105 ++++++++++++++---- .../test/resources/plugin_fixture_test.zip | Bin 3412 -> 1939 bytes pokemongo_bot/test/resources/plugin_sha/.sha | 1 + ...54eddde33061be9b329efae0cfb9bd58842655.zip | Bin 0 -> 1734 bytes 5 files changed, 140 insertions(+), 30 deletions(-) create mode 100644 pokemongo_bot/test/resources/plugin_sha/.sha create mode 100644 pokemongo_bot/test/resources/test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip diff --git a/pokemongo_bot/plugin_loader.py b/pokemongo_bot/plugin_loader.py index 7a838bb209..f7e12a85a7 100644 --- a/pokemongo_bot/plugin_loader.py +++ b/pokemongo_bot/plugin_loader.py @@ -3,6 +3,8 @@ import importlib import re import requests +import zipfile +import shutil class PluginLoader(object): folder_cache = [] @@ -20,10 +22,11 @@ def _get_correct_path(self, path): def load_plugin(self, plugin): github_plugin = GithubPlugin(plugin) if github_plugin.is_valid_plugin(): - if not github_plugin.is_already_downloaded(): - github_plugin.download() + if not github_plugin.is_already_installed(): + github_plugin.install() + + correct_path = github_plugin.get_plugin_folder() - correct_path = github_plugin.get_local_destination() else: correct_path = self._get_correct_path(plugin) @@ -42,6 +45,8 @@ def get_class(self, namespace_class): return getattr(my_module, class_name) class GithubPlugin(object): + PLUGINS_FOLDER = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'plugins') + def __init__(self, plugin_name): self.plugin_name = plugin_name self.plugin_parts = self.get_github_parts() @@ -62,18 +67,45 @@ def get_github_parts(self): return parts + def get_installed_version(self): + if not self.is_already_installed(): + return None + + filename = os.path.join(self.get_plugin_folder(), '.sha') + print filename + with open(filename) as file: + return file.read().strip() + def get_local_destination(self): parts = self.plugin_parts if parts is None: raise Exception('Not a valid github plugin') file_name = '{}_{}_{}.zip'.format(parts['user'], parts['repo'], parts['sha']) - full_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'plugins', file_name) + full_path = os.path.join(self.PLUGINS_FOLDER, file_name) return full_path - def is_already_downloaded(self): - file_path = self.get_local_destination() - return os.path.isfile(file_path) + def is_already_installed(self): + file_path = self.get_plugin_folder() + if not os.path.isdir(file_path): + return False + + sha_file = os.path.join(file_path, '.sha') + + if not os.path.isfile(sha_file): + return False + + with open(sha_file) as file: + content = file.read().strip() + + if content != self.plugin_parts['sha']: + return False + + return True + + def get_plugin_folder(self): + folder_name = '{}_{}'.format(self.plugin_parts['user'], self.plugin_parts['repo']) + return os.path.join(self.PLUGINS_FOLDER, folder_name) def get_github_download_url(self): parts = self.plugin_parts @@ -83,6 +115,24 @@ def get_github_download_url(self): github_url = 'https://github.com/{}/{}/archive/{}.zip'.format(parts['user'], parts['repo'], parts['sha']) return github_url + def install(self): + self.download() + self.extract() + + def extract(self): + dest = self.get_plugin_folder() + with zipfile.ZipFile(self.get_local_destination(), "r") as z: + z.extractall(dest) + + github_folder = os.path.join(dest, '{}-{}'.format(self.plugin_parts['repo'], self.plugin_parts['sha'])) + new_folder = os.path.join(dest, '{}'.format(self.plugin_parts['repo'])) + shutil.move(github_folder, new_folder) + + with open(os.path.join(dest, '.sha'), 'w') as file: + file.write(self.plugin_parts['sha']) + + os.remove(self.get_local_destination()) + def download(self): url = self.get_github_download_url() dest = self.get_local_destination() diff --git a/pokemongo_bot/test/plugin_loader_test.py b/pokemongo_bot/test/plugin_loader_test.py index 0b0f7da9d1..ed285ede67 100644 --- a/pokemongo_bot/test/plugin_loader_test.py +++ b/pokemongo_bot/test/plugin_loader_test.py @@ -11,6 +11,8 @@ from pokemongo_bot.plugin_loader import PluginLoader, GithubPlugin from pokemongo_bot.test.resources.plugin_fixture import FakeTask +PLUGIN_PATH = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins')) + class PluginLoaderTest(unittest.TestCase): def setUp(self): self.plugin_loader = PluginLoader() @@ -26,31 +28,39 @@ def test_load_zip(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture_test.zip') self.plugin_loader.load_plugin(package_path) loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') - self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTaskZip') self.plugin_loader.remove_path(package_path) - def copy_zip(self): - zip_fixture = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture_test.zip') - dest_path = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins', 'org_repo_sha.zip')) - shutil.copyfile(zip_fixture, dest_path) + def copy_plugin(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') + dest_path = os.path.join(PLUGIN_PATH, 'org_repo', 'plugin_fixture_tests') + shutil.copytree(package_path, os.path.join(dest_path)) + with open(os.path.join(os.path.dirname(dest_path), '.sha'), 'w') as file: + file.write('testsha') return dest_path def test_load_github_already_downloaded(self): - dest_path = self.copy_zip() - self.plugin_loader.load_plugin('org/repo#sha') - loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') + dest_path = self.copy_plugin() + self.plugin_loader.load_plugin('org/repo#testsha') + loaded_class = self.plugin_loader.get_class('plugin_fixture_tests.FakeTask') self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') self.plugin_loader.remove_path(dest_path) - os.remove(dest_path) + shutil.rmtree(os.path.dirname(dest_path)) + + def copy_zip(self): + zip_name = 'test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip' + fixture_zip = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', zip_name) + zip_dest = os.path.join(PLUGIN_PATH, 'org_test-pgo-plugin_2d54eddde33061be9b329efae0cfb9bd58842655.zip') + shutil.copyfile(fixture_zip, zip_dest) @mock.patch.object(GithubPlugin, 'download', copy_zip) def test_load_github_not_downloaded(self): - self.plugin_loader.load_plugin('org/repo#sha') - loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') - self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') - dest_path = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins', 'org_repo_sha.zip')) - self.plugin_loader.remove_path(dest_path) - os.remove(dest_path) + self.plugin_loader.load_plugin('org/test-pgo-plugin#2d54eddde33061be9b329efae0cfb9bd58842655') + loaded_class = self.plugin_loader.get_class('test-pgo-plugin.PrintText') + self.assertEqual(loaded_class({}, {}).work(), 'PrintText') + dest_path = os.path.join(PLUGIN_PATH, 'org_test-pgo-plugin') + self.plugin_loader.remove_path(os.path.join(dest_path, 'test-pgo-plugin')) + shutil.rmtree(dest_path) class GithubPluginTest(unittest.TestCase): def test_get_github_parts_for_valid_github(self): @@ -65,10 +75,25 @@ def test_get_github_parts_for_invalid_github(self): self.assertFalse(GithubPlugin('foo').is_valid_plugin()) self.assertFalse(GithubPlugin('/Users/foo/bar.zip').is_valid_plugin()) + def test_get_installed_version(self): + github_plugin = GithubPlugin('org/repo#my-version') + src_fixture = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_sha') + dest = github_plugin.get_plugin_folder() + shutil.copytree(src_fixture, dest) + actual = github_plugin.get_installed_version() + shutil.rmtree(dest) + self.assertEqual('my-version', actual) + + def test_get_plugin_folder(self): + github_plugin = GithubPlugin('org/repo#sha') + expected = os.path.join(PLUGIN_PATH, 'org_repo') + actual = github_plugin.get_plugin_folder() + self.assertEqual(actual, expected) + def test_get_local_destination(self): github_plugin = GithubPlugin('org/repo#sha') path = github_plugin.get_local_destination() - expected = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins', 'org_repo_sha.zip')) + expected = os.path.join(PLUGIN_PATH, 'org_repo_sha.zip') self.assertEqual(path, expected) def test_get_github_download_url(self): @@ -77,13 +102,47 @@ def test_get_github_download_url(self): expected = 'https://github.com/org/repo/archive/sha.zip' self.assertEqual(url, expected) - def test_is_already_downloaded_not_downloaded(self): + def test_is_already_installed_not_installed(self): + github_plugin = GithubPlugin('org/repo#sha') + self.assertFalse(github_plugin.is_already_installed()) + + def test_is_already_installed_version_mismatch(self): github_plugin = GithubPlugin('org/repo#sha') - self.assertFalse(github_plugin.is_already_downloaded()) + plugin_folder = github_plugin.get_plugin_folder() + os.mkdir(plugin_folder) + with open(os.path.join(plugin_folder, '.sha'), 'w') as file: + file.write('sha2') - def test_is_already_downloaded_downloaded(self): + actual = github_plugin.is_already_installed() + shutil.rmtree(plugin_folder) + self.assertFalse(actual) + + def test_is_already_installed_installed(self): github_plugin = GithubPlugin('org/repo#sha') - dest = github_plugin.get_local_destination() - open(dest, 'a').close() - self.assertTrue(github_plugin.is_already_downloaded()) - os.remove(dest) + plugin_folder = github_plugin.get_plugin_folder() + os.mkdir(plugin_folder) + with open(os.path.join(plugin_folder, '.sha'), 'w') as file: + file.write('sha') + + actual = github_plugin.is_already_installed() + shutil.rmtree(plugin_folder) + self.assertTrue(actual) + + def test_extract(self): + github_plugin = GithubPlugin('org/test-pgo-plugin#2d54eddde33061be9b329efae0cfb9bd58842655') + src = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip') + zip_dest = github_plugin.get_local_destination() + shutil.copyfile(src, zip_dest) + github_plugin.extract() + plugin_folder = github_plugin.get_plugin_folder() + os.path.isdir(plugin_folder) + sub_folder = os.path.join(plugin_folder, 'test-pgo-plugin') + os.path.isdir(sub_folder) + sha_file = os.path.join(github_plugin.get_plugin_folder(), '.sha') + os.path.isfile(sha_file) + + with open(sha_file) as file: + content = file.read().strip() + self.assertEqual(content, '2d54eddde33061be9b329efae0cfb9bd58842655') + + shutil.rmtree(plugin_folder) diff --git a/pokemongo_bot/test/resources/plugin_fixture_test.zip b/pokemongo_bot/test/resources/plugin_fixture_test.zip index 335d95e5226056902bb02114fa26a2b3dad61ca1..78828798c32b46cb56eac6df953e7add47c175cf 100644 GIT binary patch delta 564 zcmca2HJP6$z?+$civa`#w@>8Jl2Daj5iSJ8Ul~OhKyo4s96%_teWIT+y8uvJTzBe z?UTP2rKskudemEWZEnUbYd?z(IW;{;`c(fVsP44FwpL~y9Tq!hyg8^X%D;p?TfTDjV69dB$77z~reUPTy delta 1852 zcmbQte?^KXz?+$civa|hB`5M|Ni>Nq4=?3h9{!b4gaITc!oUH9*^(3ejM+PYa-N*a zCw5$9`lT>Ano(W?Bm%-P)ew__l9L#XF(hF|O}@)$2QzE35>pp*wb=5>>zSgNnk6Um zFsn_T%fwxu92&vH02GJ2C3J$f7mJ}loA%u%egPMortW(gdJ2p5N|tt5b!_I$Ek7$@ zeOEL{^n!4u1OI}lnT|R^@;_Jqvw6M#@7~*Q&p+p@TYPEHg(u0b>km6HNbu_L&RC?) zA=+cAlYQ0H#w!PKT7Ud{i%gXhJ}3-CneNN*1o)(D|TvK($X^Z zsA>)K3CgbjKJPaBwKKXb`+@uq*F^E2PF=SA1-Y9&tvsxub#AZ$Ld+zUtRsO=&js}f~1>2B)a8U!`+N;<}#bJOulBY_=Y|AH+GN< zjqOD|Yk@AD1jLBM(JaaC2^4?>MFxS%A6Y~vvGZU?Ha(dCfU_y# zJCoA3DAyxqDr=MKrkUKFGGmGKrsymAd%M}cFj#HQ?2cRDU3TL3+{*mty}xU3RNwet z<(05E=FapTUp3oA?AcTujw&P_2x3=h?s0r|Nc{l&f#N3)w;C_pKK$hmd;Rp|AI{4p zNF0muOOA~w5Ed89Y1PQ}a5^rbB4B??P*(ZfiB^R#GD%C%w5;{ch!2cpfAjd(oI;1^ zav!(-Dp{R6bE~^ZNlWATEjcFBQ_sD2+k4_j#=F%{VOf={&$XTUu*;QO^;t*Hl{WLB z)uEF;&b}_%dSbfL>HB9^O|&^YujSyf`d?pnht`DpUR2dRUiPBYER17gkG5vZ%gBJ0 zC80t#juPi;>VXr zkcQ=D^yt!oMAu{i7IDn@IvR8v9$yOkIO-e)-u!FmSSu#Dk=Jwejp?en+f>a?>fPjh zwfSGS{0sIMXSRiQ*}a}*`=rIP_?giE+U=#A|CX7zUj6uS-a}J&4oy3rqK2k}GZ?*X zC-9{41j)xS#xcA*F1LiYqgj{de7)TV7M}ZW_?*lQi)PGdz3XnI<-2z4sx>~=R*TQa zPg8j@=_z9qOOaEu*O?QkZ=6Ng<({hFm@s$p8pd^JQhn~TvVIWeS;l5(w8<#undJ4U zRTF}Ydi_P#q@3e)-CQ>TgPkuMlHMQZ^MeEQ*)o>`xm_S6ASG0`%&M$pmM&Bdd;b8 zr*C|=V+6&S>c+C#5MZ2r1IHO7lPEJ60~;t00ZYM4EEZz0f;hk%BnK}$6<8g`pyhBl zL<(N!mb2=Dr9jeXQoEqK5K?dh1=)Pyg>ed7kP@uKLADo^WDup(Z@U diff --git a/pokemongo_bot/test/resources/plugin_sha/.sha b/pokemongo_bot/test/resources/plugin_sha/.sha new file mode 100644 index 0000000000..eaf604c9ac --- /dev/null +++ b/pokemongo_bot/test/resources/plugin_sha/.sha @@ -0,0 +1 @@ +my-version diff --git a/pokemongo_bot/test/resources/test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip b/pokemongo_bot/test/resources/test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip new file mode 100644 index 0000000000000000000000000000000000000000..a692ac3f08492882a28820569a62b59d2647006f GIT binary patch literal 1734 zcmWIWW@h1H00EUv?4Do-l(1yrWGG23F3~MW&(|%;DNWDJ(=|#lHAziLNl7&}HZU_x zO0`TfHnL1jOH4ILPD`>(N-?#tFflSSHPsIdVP#;vD!L*(0BSx5*!)Ruz2?453=EZY+*mHVk{C#!H>m@IgCcmF3;%OWIa(3v7jeA_4XI_~5Oe3#w|LWT6`t|M)BHw;5 z+n=YH5|>_-JvUN%$C}s*HSH>gtm6wN z>+z<~X`LdrR!?om#7$FL-8LR)Hxx0cST=G(^n!Ao*PN!P%_x4c&Yp4}6yT<(=;iumAbz&AQdA zZ&miDu6pyqY{&0vBgJIj^}ix^Xmn;rZ$Gc*vu3l*@sG+0XBOK;KmNYMRbe{M*{KKg z1#UkS3ZHKp;j~)+igT;V&YQQKUzpGKiP7JfyDm@l(VIh&{5k#HtSfmP)6B|$hs%cL z-Vu>b?(h`$WPNXT_7%I>tC{=(VKL78kN18PkUSq&-?GE$%Bs6dE+vRQovzVx^zsr> zAwwqCvnDcs)l9Y;wr1$LcuEumYKHRdP@m2srF(DXKlAzWbtShww4<&X7K&a;&784I zXnjTJpHmb62~TH;U-XXwHQCl5-?K;#m@VXi*oBg08y}yUmst`YuUAkBPq*IMr_P6- zSlgqoaq5hA;E6N3C-k)fIT#9^g0#JMpgF|$*O_^9fsQy&sY7xr;|q#1^Gc8%;@jiT z-Q*y0&rU{@JX(=-uSI>R&%z*2v5=-aj+ac?0i)BdZKc zIh0>MFi`i2ev^10>}y8W6xK`YPRyM&Ww+R@ItTB9c}#|t$4=>$^X Date: Mon, 8 Aug 2016 13:49:24 -0700 Subject: [PATCH 030/143] Dev merge to master, PR (#3146) * Adding plugin support (#2679) * Adding plugin support * Adding an empty __init__.py * Moving the base task to the project root (#2702) * Moving the base task to the project root * Moving the base class more * Changing the import again * Adding a heartbeat to the analytics (#2709) * Adding a heartbeat to the analytics * Heartbeat every 30 seconds, not every 5 * Don't double track clients * Fix 'local variable 'bot' referenced before assignment' * Providing an error if tasks don't work for the given api (#2732) * Fix for utf8 encoding when catching lured pokemon (#2720) * Fixing lure pokestop encoding * fixing lure encoding * Fix For catchable not being displayed on the web (#2719) * Fix For catchable not being displayed on the web * Update catch_visible_pokemon.py * Added encrypt.so compilation process to Dockerfile (#2695) * OS Detection for encrypt lib (#2768) Fix 32bit check, darwin and linux use the same file Make it a function Check if file exists, if not show error Define file_name first Fix return Check if file exists, if not show error Print info about paths Fix for 32/64bit detection * Fix Typo in unexpected_response_retry (#2531) fixes #2525 #2523 * Revert "changing license from MIT to GPLv3" This reverts commit 69fb64f2bf7c12e28c2bb6d2b636c6af55822448. * When the google analytics domain is blocked the bot crashed. (#2764) With a simple try / except this can be solved. Fix dirty catch all * Fixes #2698 - Prevents "Possibly searching too often" error after re-login. (#2771) * Fixes #2698 - Added api.activate_signature call to prevent issue after re-login. - Also replaced deprecated log call with event_manager emit to prevent exception being thrown. * Modified to use OS detected library path as per PR #2768 * Support loading plugins from .zip files (#2766) * Keep track of how many pokemon released (#2884) * Setting Library path to work with encrypt.so (#2899) Setting LD_LIBRARY_PATH on Dockerfile * :sparkles: Added login and username to available stats (#2494) Added a player_data property in PokemonGoBot to access player data from outside Added unit tests for login and username stats Added tests for call args when updating the window title Added a platform-specific test for window title updating on win32 platform * [dev] small fixes (#2912) * Fixed emit_event typo * Update CONTRIBUTORS.md * Changed initialization location for "bot" We use bot in main exception on 128 * Update pokecli.py * Rename load_path to load_plugin (#2947) * Adding some logic for pulling plugins from github (#2967) * flush after title update (#2977) * correctly re-raise exception to keep backtrace (#2944) * Update MoveToMapPokemon to use events instead of logger. (#2913) * Config/encrypt.so (#2964) * Add config option for libencrypt.so * Correctly set the config value and check for the file in said dir * Fixed mispelling for "formatted" variable (#2984) * Loading plugins from Github (#2992) * Checking github plugin file existence * Loading plugins from github * Fixed #3000 (#3003) Fixed syntax error on "move_to_map_pokemon.py" that makes the client crash when using this feature. * Added MaxPotion inventory count to summary. (#3015) Short Description: The Max Potion count was missing from the inventory summary. Was #2456 * Added cleanup of download and files for encrypt.so after they are no longer needed (#3011) * Fix bot not returning back after telepoting (#3014) * Fix typo: last_long -> last_lon * Whitespace cleanup * Fix bug introduced by #3037: bot not returning back * Fix Dockerfile installation (#3057) * Fix for #3045 (#3055) * Added request to check configuration (#3089) * Fixed Dockerfile - missing \ on command lines (#3096) * Fixed mispelling for "formatted" variable * Docker commands missing trailing \ * Fix for FileIO slowing bot performance.This puts the map writing into a thread and makes sure it only executes once. (#3100) * Change word usage: "fled" to "escaped" (#3118) "fled" is confusing to lot of people and is easily confused with pokemon vanishing. "escaped" is a better term. * Update the example config file (#3120) * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Rename path.example.json to path.json.example * typo: logrmation -> information (#2601) Fix a typo. I assume that it was "information" initially, but became "logrmation" when someone used replace all functionality to replace all infos with logs. But I might be totally wrong at this point, idk. Just didn't like the word and wanted to fix that typo. * Change fled to escaped (#3129) Fix an issue after PR #3118 * When JSON parsing fails, give a rough indication of why (#3137) * When JSON parsing fails, give a rough indication of why * Use the official package instead of SHA1 commit * Handle Github Download Zip Format (#3108) * Checking github plugin file existence * Loading plugins from github * Starting install code for github plugins * Updating GithubPlugin to support extracting folders * Handling github zip formats by extracting to the correct location --- .github/ISSUE_TEMPLATE.md | 2 + .gitignore | 5 + CONTRIBUTORS.md | 1 + Dockerfile | 4 +- configs/config.json.cluster.example | 1 + configs/config.json.example | 1 + configs/config.json.map.example | 1 + configs/config.json.path.example | 1 + configs/config.json.pokemon.example | 1 + .../{path.example.json => path.json.example} | 0 pokecli.py | 35 ++- pokemongo_bot/__init__.py | 65 ++++- .../cell_workers/move_to_map_pokemon.py | 227 +++++++++++++++--- .../cell_workers/pokemon_catch_worker.py | 4 +- .../cell_workers/update_title_stats.py | 2 + pokemongo_bot/health_record/bot_event.py | 2 +- pokemongo_bot/plugin_loader.py | 119 ++++++++- pokemongo_bot/plugins/.keep | 1 + pokemongo_bot/test/plugin_loader_test.py | 128 +++++++++- .../test/resources/plugin_fixture_test.zip | Bin 3412 -> 1939 bytes pokemongo_bot/test/resources/plugin_sha/.sha | 1 + ...54eddde33061be9b329efae0cfb9bd58842655.zip | Bin 0 -> 1734 bytes requirements.txt | 1 + tests/tree_config_builder_test.py | 4 +- 24 files changed, 546 insertions(+), 60 deletions(-) rename configs/{path.example.json => path.json.example} (100%) create mode 100644 pokemongo_bot/plugins/.keep create mode 100644 pokemongo_bot/test/resources/plugin_sha/.sha create mode 100644 pokemongo_bot/test/resources/test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 9976991cf9..a1d7168e75 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,5 @@ +Please check configuration at http://jsonlint.com/ before posting an issue. + ### Expected Behavior diff --git a/.gitignore b/.gitignore index a12509c322..70ab34b250 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,9 @@ var/ .pydevproject .settings/ +# Cloud9 Users +.c9/ + # Installer logs pip-log.txt pip-delete-this-directory.txt @@ -86,6 +89,8 @@ celerybeat-schedule # virtualenv venv/ ENV/ +local/ +share/ # Spyder project settings .spyderproject diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ee5aa7063d..6dceaf0918 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -53,3 +53,4 @@ * lucasfevi * Moonlight-Angel * mjmadsen + * nikofil diff --git a/Dockerfile b/Dockerfile index dce398c63e..f98d5d6942 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,9 @@ RUN cd /tmp && wget "http://pgoapi.com/pgoencrypt.tar.gz" \ && tar zxvf pgoencrypt.tar.gz \ && cd pgoencrypt/src \ && make \ - && cp libencrypt.so /usr/src/app/encrypt.so + && cp libencrypt.so /usr/src/app/encrypt.so \ + && cd /tmp \ + && rm -rf /tmp/pgoencrypt* VOLUME ["/usr/src/app/web"] diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index 8d0d8f854f..b32eb4f668 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.example b/configs/config.json.example index 20ef72e34e..ec46c15eb9 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.map.example b/configs/config.json.map.example index e665d4c6da..1079c999f9 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.path.example b/configs/config.json.path.example index afd1e3afeb..94a9fdba07 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 7cad1ac066..1d428a6ae7 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "libencrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/path.example.json b/configs/path.json.example similarity index 100% rename from configs/path.example.json rename to configs/path.json.example diff --git a/pokecli.py b/pokecli.py index 24a0f38ee3..55afa55399 100644 --- a/pokecli.py +++ b/pokecli.py @@ -42,6 +42,12 @@ from pokemongo_bot.health_record import BotEvent from pokemongo_bot.plugin_loader import PluginLoader +try: + from demjson import jsonlint +except ImportError: + # Run `pip install -r requirements.txt` to fix this + jsonlint = None + if sys.version_info >= (2, 7, 9): ssl._create_default_https_context = ssl._create_unverified_context @@ -53,7 +59,7 @@ def main(): bot = False - + try: logger.info('PokemonGO Bot v1.0') sys.stdout = codecs.getwriter('utf8')(sys.stdout) @@ -104,7 +110,7 @@ def main(): 'api_error', sender=bot, level='info', - formmated='Log logged in, reconnecting in {:s}'.format(wait_time) + formatted='Log logged in, reconnecting in {:d}'.format(wait_time) ) time.sleep(wait_time) except ServerBusyOrOfflineException: @@ -130,7 +136,7 @@ def main(): if bot: report_summary(bot) - raise e + raise def report_summary(bot): if bot.metrics.start_time is None: @@ -162,16 +168,28 @@ def init_config(): # If config file exists, load variables from json load = {} + def _json_loader(filename): + try: + with open(filename, 'rb') as data: + load.update(json.load(data)) + except ValueError: + if jsonlint: + with open(filename, 'rb') as data: + lint = jsonlint() + rc = lint.main(['-v', filename]) + + logger.critical('Error with configuration file') + sys.exit(-1) + # Select a config file code parser.add_argument("-cf", "--config", help="Config File to use") config_arg = parser.parse_known_args() and parser.parse_known_args()[0].config or None + if config_arg and os.path.isfile(config_arg): - with open(config_arg) as data: - load.update(json.load(data)) + _json_loader(config_arg) elif os.path.isfile(config_file): logger.info('No config argument specified, checking for /configs/config.json') - with open(config_file) as data: - load.update(json.load(data)) + _json_loader(config_file) else: logger.info('Error: No /configs/config.json or specified config') @@ -384,6 +402,7 @@ def init_config(): if not config.password and 'password' not in load: config.password = getpass("Password: ") + config.encrypt_location = load.get('encrypt_location','') config.catch = load.get('catch', {}) config.release = load.get('release', {}) config.action_wait_max = load.get('action_wait_max', 4) @@ -446,7 +465,7 @@ def task_configuration_error(flag_name): plugin_loader = PluginLoader() for plugin in config.plugins: - plugin_loader.load_path(plugin) + plugin_loader.load_plugin(plugin) # create web dir if not exists try: diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index d0f034f110..8e9a28ce3a 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -9,6 +9,8 @@ import re import sys import time +import Queue +import threading from geopy.geocoders import GoogleV3 from pgoapi import PGoApi @@ -69,6 +71,11 @@ def __init__(self, config): # Make our own copy of the workers for this instance self.workers = [] + # Theading setup for file writing + self.web_update_queue = Queue.Queue(maxsize=1) + self.web_update_thread = threading.Thread(target=self.update_web_location_worker) + self.web_update_thread.start() + def start(self): self._setup_event_system() self._setup_logging() @@ -249,7 +256,7 @@ def _register_events(self): ) ) self.event_manager.register_event( - 'pokemon_fled', + 'pokemon_escaped', parameters=('pokemon',) ) self.event_manager.register_event( @@ -393,6 +400,35 @@ def _register_events(self): ) self.event_manager.register_event('unset_pokemon_nickname') + # Move To map pokemon + self.event_manager.register_event( + 'move_to_map_pokemon_fail', + parameters=('message',) + ) + self.event_manager.register_event( + 'move_to_map_pokemon_updated_map', + parameters=('lat', 'lon') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_teleport_to', + parameters=('poke_name', 'poke_dist', 'poke_lat', 'poke_lon', + 'disappears_in') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_encounter', + parameters=('poke_name', 'poke_dist', 'poke_lat', 'poke_lon', + 'disappears_in') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_move_towards', + parameters=('poke_name', 'poke_dist', 'poke_lat', 'poke_lon', + 'disappears_in') + ) + self.event_manager.register_event( + 'move_to_map_pokemon_teleport_back', + parameters=('last_lat', 'last_lon') + ) + def tick(self): self.health_record.heartbeat() self.cell = self.get_meta_cell() @@ -607,7 +643,6 @@ def login(self): ) def get_encryption_lib(self): - file_name = '' if _platform == "linux" or _platform == "linux2" or _platform == "darwin": file_name = 'encrypt.so' elif _platform == "Windows" or _platform == "win32": @@ -617,15 +652,18 @@ def get_encryption_lib(self): else: file_name = 'encrypt.dll' - path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) - full_path = path + '/'+ file_name + if self.config.encrypt_location == '': + path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) + else: + path = self.config.encrypt_location + full_path = path + '/'+ file_name if not os.path.isfile(full_path): - self.logger.error(file_name + ' is not found! Please place it in the bots root directory.') - self.logger.info('Platform: '+ _platform + ' Bot root directory: '+ path) + self.logger.error(file_name + ' is not found! Please place it in the bots root directory or set libencrypt_location in config.') + self.logger.info('Platform: '+ _platform + ' Encrypt.so directory: '+ path) sys.exit(1) else: - self.logger.info('Found '+ file_name +'! Platform: ' + _platform + ' Bot root directory: ' + path) + self.logger.info('Found '+ file_name +'! Platform: ' + _platform + ' Encrypt.so directory: ' + path) return full_path @@ -716,7 +754,8 @@ def _print_character_info(self): self.logger.info( 'Potion: ' + str(items_stock[101]) + ' | SuperPotion: ' + str(items_stock[102]) + - ' | HyperPotion: ' + str(items_stock[103])) + ' | HyperPotion: ' + str(items_stock[103]) + + ' | MaxPotion: ' + str(items_stock[104])) self.logger.info( 'Incense: ' + str(items_stock[401]) + @@ -944,7 +983,15 @@ def heartbeat(self): request.get_player() request.check_awarded_badges() request.call() - self.update_web_location() # updates every tick + try: + self.web_update_queue.put_nowait(True) # do this outside of thread every tick + except Queue.Full: + pass + + def update_web_location_worker(self): + while True: + self.web_update_queue.get() + self.update_web_location() def get_inventory_count(self, what): response_dict = self.get_inventory() diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 08ff35f281..9ab6bbb7a2 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -1,11 +1,59 @@ # -*- coding: utf-8 -*- +""" +Moves a trainer to a Pokemon. + +Events: + move_to_map_pokemon_fail + When the worker fails. + Returns: + message: Failure message. + + move_to_map_pokemon_updated_map + When worker updates the PokemonGo-Map. + Returns: + lat: Latitude + lon: Longitude + + move_to_map_pokemon_teleport_to + When trainer is teleported to a Pokemon. + Returns: + poke_name: Pokemon's name + poke_dist: Distance from the trainer + poke_lat: Latitude of the Pokemon + poke_lon: Longitude of the Pokemon + disappears_in: Number of seconds before the Pokemon disappears + + move_to_map_pokemon_encounter + When a trainer encounters a Pokemon by teleporting or walking. + Returns: + poke_name: Pokemon's name + poke_dist: Distance from the trainer + poke_lat: Latitude of the Pokemon + poke_lon: Longitude of the Pokemon + disappears_in: Number of seconds before the Pokemon disappears + + move_to_map_pokemon_move_towards + When a trainer moves toward a Pokemon. + Returns: + poke_name: Pokemon's name + poke_dist: Distance from the trainer + poke_lat: Latitude of the Pokemon + poke_lon: Longitude of the Pokemon + disappears_in: Number of seconds before the Pokemon disappears + + move_to_map_pokemon_teleport_back + When a trainer teleports back to thier previous location. + Returns: + last_lat: Trainer's last known latitude + last_lon: Trainer's last known longitude + +""" import os import time import json import base64 import requests -from pokemongo_bot import logger from pokemongo_bot.cell_workers.utils import distance, format_dist, format_time from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult @@ -13,7 +61,20 @@ from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker +# Update the map if more than N meters away from the center. (AND'd with +# UPDATE_MAP_MIN_TIME_MINUTES) +UPDATE_MAP_MIN_DISTANCE_METERS = 500 + +# Update the map if it hasn't been updated in n seconds. (AND'd with +# UPDATE_MAP_MIN_DISTANCE_METERS) +UPDATE_MAP_MIN_TIME_SEC = 120 + +# Number of seconds to sleep between teleporting to a snipped Pokemon. +SNIPE_SLEEP_SEC = 2 + + class MoveToMapPokemon(BaseTask): + """Task for moving a trainer to a Pokemon.""" SUPPORTED_TASK_API_VERSION = 1 def initialize(self): @@ -32,13 +93,15 @@ def get_pokemon_from_map(self): try: req = requests.get('{}/raw_data?gyms=false&scanned=false'.format(self.config['address'])) except requests.exceptions.ConnectionError: - logger.log('Could not reach PokemonGo-Map Server', 'red') + self._emit_failure('Could not get Pokemon data from PokemonGo-Map: ' + '{}. Is it running?'.format( + self.config['address'])) return [] try: raw_data = req.json() except ValueError: - logger.log('Map data was not valid', 'red') + self._emit_failure('Map data was not valid') return [] pokemon_list = [] @@ -48,7 +111,7 @@ def get_pokemon_from_map(self): try: pokemon['encounter_id'] = long(base64.b64decode(pokemon['encounter_id'])) except TypeError: - log.logger('base64 error: {}'.format(pokemon['encounter_id']), 'red') + self._emit_failure('base64 error: {}'.format(pokemon['encounter_id'])) continue pokemon['spawn_point_id'] = pokemon['spawnpoint_id'] pokemon['disappear_time'] = int(pokemon['disappear_time'] / 1000) @@ -100,14 +163,17 @@ def update_map_location(self): try: req = requests.get('{}/loc'.format(self.config['address'])) except requests.exceptions.ConnectionError: - logger.log('Could not reach PokemonGo-Map Server', 'red') + self._emit_failure('Could not update trainer location ' + 'PokemonGo-Map: {}. Is it running?'.format( + self.config['address'])) return try: loc_json = req.json() except ValueError: - return log.logger('Map location data was not valid', 'red') - + err = 'Map location data was not valid' + self._emit_failure(err) + return log.logger(err, 'red') dist = distance( self.bot.position[0], @@ -118,32 +184,40 @@ def update_map_location(self): # update map when 500m away from center and last update longer than 2 minutes away now = int(time.time()) - if dist > 500 and now - self.last_map_update > 2 * 60: - requests.post('{}/next_loc?lat={}&lon={}'.format(self.config['address'], self.bot.position[0], self.bot.position[1])) - logger.log('Updated PokemonGo-Map position') + if (dist > UPDATE_MAP_MIN_DISTANCE_METERS and + now - self.last_map_update > UPDATE_MAP_MIN_TIME_SEC): + requests.post( + '{}/next_loc?lat={}&lon={}'.format(self.config['address'], + self.bot.position[0], + self.bot.position[1])) + self.emit_event( + 'move_to_map_pokemon_updated_map', + formatted='Updated PokemonGo-Map to {lat}, {lon}', + data={ + 'lat': self.bot.position[0], + 'lon': self.bot.position[1] + } + ) self.last_map_update = now def snipe(self, pokemon): - last_position = self.bot.position[0:2] + """Snipe a Pokemon by teleporting. + Args: + pokemon: Pokemon to snipe. + """ + last_position = self.bot.position[0:2] self.bot.heartbeat() - - logger.log('Teleporting to {} ({})'.format(pokemon['name'], format_dist(pokemon['dist'], self.unit)), 'green') - self.bot.api.set_position(pokemon['latitude'], pokemon['longitude'], 0) - - logger.log('Encounter pokemon', 'green') + self._teleport_to(pokemon) catch_worker = PokemonCatchWorker(pokemon, self.bot) api_encounter_response = catch_worker.create_encounter_api_call() - - time.sleep(2) - logger.log('Teleporting back to previous location..', 'green') + time.sleep(SNIPE_SLEEP_SEC) + self._teleport_back(last_position) self.bot.api.set_position(last_position[0], last_position[1], 0) - time.sleep(2) + time.sleep(SNIPE_SLEEP_SEC) self.bot.heartbeat() - catch_worker.work(api_encounter_response) self.add_caught(pokemon) - return WorkerResult.SUCCESS def dump_caught_pokemon(self): @@ -182,18 +256,109 @@ def work(self): if self.config['snipe']: return self.snipe(pokemon) + step_walker = self._move_to(pokemon) + if not step_walker.step(): + return WorkerResult.RUNNING + self._encountered(pokemon) + self.add_caught(pokemon) + return WorkerResult.SUCCESS + + def _emit_failure(self, msg): + """Emits failure to event log. + + Args: + msg: Message to emit + """ + self.emit_event( + 'move_to_map_pokemon_fail', + formatted='Failure! {message}', + data={'message': msg} + ) + + def _emit_log(self, msg): + """Emits log to event log. + + Args: + msg: Message to emit + """ + self.emit_event( + 'move_to_map_pokemon', + formatted='{message}', + data={'message': msg} + ) + + def _pokemon_event_data(self, pokemon): + """Generates parameters used for the Bot's event manager. + + Args: + pokemon: Pokemon object + + Returns: + Dictionary with Pokemon's info. + """ now = int(time.time()) - logger.log('Moving towards {}, {} left ({})'.format(pokemon['name'], format_dist(pokemon['dist'], self.unit), format_time(pokemon['disappear_time'] - now))) - step_walker = StepWalker( + return { + 'poke_name': pokemon['name'], + 'poke_dist': (format_dist(pokemon['dist'], self.unit)), + 'poke_lat': pokemon['latitude'], + 'poke_lon': pokemon['longitude'], + 'disappears_in': (format_time(pokemon['disappear_time'] - now)) + } + + def _teleport_to(self, pokemon): + """Teleports trainer to a Pokemon. + + Args: + pokemon: Pokemon to teleport to. + """ + self.emit_event( + 'move_to_map_pokemon_teleport_to', + formatted='Teleporting to {poke_name}. ({poke_dist})', + data=self._pokemon_event_data(pokemon) + ) + self.bot.api.set_position(pokemon['latitude'], pokemon['longitude'], 0) + self._encountered(pokemon) + + def _encountered(self, pokemon): + """Emit event when trainer encounters a Pokemon. + + Args: + pokemon: Pokemon encountered. + """ + self.emit_event( + 'move_to_map_pokemon_encounter', + formatted='Encountered Pokemon: {poke_name}', + data=self._pokemon_event_data(pokemon) + ) + + def _teleport_back(self, last_position): + """Teleports trainer back to their last position.""" + self.emit_event( + 'move_to_map_pokemon_teleport_back', + formatted=('Teleporting back to previous location ({last_lat}, ' + '{last_lon})'), + data={'last_lat': last_position[0], 'last_lon': last_position[1]} + ) + + def _move_to(self, pokemon): + """Moves trainer towards a Pokemon. + + Args: + pokemon: Pokemon to move to. + + Returns: + StepWalker + """ + now = int(time.time()) + self.emit_event( + 'move_to_map_pokemon_move_towards', + formatted=('Moving towards {poke_name}, {poke_dist}, left (' + '{disappears_in})'), + data=self._pokemon_event_data(pokemon) + ) + return StepWalker( self.bot, self.bot.config.walk, pokemon['latitude'], pokemon['longitude'] ) - - if not step_walker.step(): - return WorkerResult.RUNNING - - logger.log('Arrived at {}'.format(pokemon['name'])) - self.add_caught(pokemon) - return WorkerResult.SUCCESS diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index d676e9f0e2..d5118b391b 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -324,8 +324,8 @@ def work(self, response_dict=None): 'CATCH_POKEMON']['status'] if status is 2: self.emit_event( - 'pokemon_fled', - formatted="{pokemon} fled.", + 'pokemon_escaped', + formatted="{pokemon} escaped.", data={'pokemon': pokemon_name} ) sleep(2) diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index 43e55c260f..acbfaa7fe4 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -111,8 +111,10 @@ def _update_title(self, title, platform): """ if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) + stdout.flush() elif platform == "darwin": stdout.write("\033]0;{}\007".format(title)) + stdout.flush() elif platform == "win32": ctypes.windll.kernel32.SetConsoleTitleA(title) else: diff --git a/pokemongo_bot/health_record/bot_event.py b/pokemongo_bot/health_record/bot_event.py index f357a4e8e7..55a726049d 100644 --- a/pokemongo_bot/health_record/bot_event.py +++ b/pokemongo_bot/health_record/bot_event.py @@ -16,7 +16,7 @@ def __init__(self, config): # UniversalAnalytics can be reviewed here: # https://github.com/analytics-pros/universal-analytics-python if self.config.health_record: - self.logger.info('Health check is enabled. For more logrmation:') + self.logger.info('Health check is enabled. For more information:') self.logger.info('https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev#analytics') self.client = Client( dsn='https://8abac56480f34b998813d831de262514:196ae1d8dced41099f8253ea2c8fe8e6@app.getsentry.com/90254', diff --git a/pokemongo_bot/plugin_loader.py b/pokemongo_bot/plugin_loader.py index 3bded030b3..f7e12a85a7 100644 --- a/pokemongo_bot/plugin_loader.py +++ b/pokemongo_bot/plugin_loader.py @@ -1,6 +1,10 @@ import os import sys import importlib +import re +import requests +import zipfile +import shutil class PluginLoader(object): folder_cache = [] @@ -15,8 +19,16 @@ def _get_correct_path(self, path): return correct_path - def load_path(self, path): - correct_path = self._get_correct_path(path) + def load_plugin(self, plugin): + github_plugin = GithubPlugin(plugin) + if github_plugin.is_valid_plugin(): + if not github_plugin.is_already_installed(): + github_plugin.install() + + correct_path = github_plugin.get_plugin_folder() + + else: + correct_path = self._get_correct_path(plugin) if correct_path not in self.folder_cache: self.folder_cache.append(correct_path) @@ -25,9 +37,112 @@ def load_path(self, path): def remove_path(self, path): correct_path = self._get_correct_path(path) sys.path.remove(correct_path) + self.folder_cache.remove(correct_path) def get_class(self, namespace_class): [namespace, class_name] = namespace_class.split('.') my_module = importlib.import_module(namespace) return getattr(my_module, class_name) +class GithubPlugin(object): + PLUGINS_FOLDER = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'plugins') + + def __init__(self, plugin_name): + self.plugin_name = plugin_name + self.plugin_parts = self.get_github_parts() + + def is_valid_plugin(self): + return self.plugin_parts is not None + + def get_github_parts(self): + groups = re.match('(.*)\/(.*)#(.*)', self.plugin_name) + + if groups is None: + return None + + parts = {} + parts['user'] = groups.group(1) + parts['repo'] = groups.group(2) + parts['sha'] = groups.group(3) + + return parts + + def get_installed_version(self): + if not self.is_already_installed(): + return None + + filename = os.path.join(self.get_plugin_folder(), '.sha') + print filename + with open(filename) as file: + return file.read().strip() + + def get_local_destination(self): + parts = self.plugin_parts + if parts is None: + raise Exception('Not a valid github plugin') + + file_name = '{}_{}_{}.zip'.format(parts['user'], parts['repo'], parts['sha']) + full_path = os.path.join(self.PLUGINS_FOLDER, file_name) + return full_path + + def is_already_installed(self): + file_path = self.get_plugin_folder() + if not os.path.isdir(file_path): + return False + + sha_file = os.path.join(file_path, '.sha') + + if not os.path.isfile(sha_file): + return False + + with open(sha_file) as file: + content = file.read().strip() + + if content != self.plugin_parts['sha']: + return False + + return True + + def get_plugin_folder(self): + folder_name = '{}_{}'.format(self.plugin_parts['user'], self.plugin_parts['repo']) + return os.path.join(self.PLUGINS_FOLDER, folder_name) + + def get_github_download_url(self): + parts = self.plugin_parts + if parts is None: + raise Exception('Not a valid github plugin') + + github_url = 'https://github.com/{}/{}/archive/{}.zip'.format(parts['user'], parts['repo'], parts['sha']) + return github_url + + def install(self): + self.download() + self.extract() + + def extract(self): + dest = self.get_plugin_folder() + with zipfile.ZipFile(self.get_local_destination(), "r") as z: + z.extractall(dest) + + github_folder = os.path.join(dest, '{}-{}'.format(self.plugin_parts['repo'], self.plugin_parts['sha'])) + new_folder = os.path.join(dest, '{}'.format(self.plugin_parts['repo'])) + shutil.move(github_folder, new_folder) + + with open(os.path.join(dest, '.sha'), 'w') as file: + file.write(self.plugin_parts['sha']) + + os.remove(self.get_local_destination()) + + def download(self): + url = self.get_github_download_url() + dest = self.get_local_destination() + + r = requests.get(url, stream=True) + r.raise_for_status() + + with open(dest, 'wb') as f: + for chunk in r.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) + r.close() + return dest diff --git a/pokemongo_bot/plugins/.keep b/pokemongo_bot/plugins/.keep new file mode 100644 index 0000000000..5d848b8301 --- /dev/null +++ b/pokemongo_bot/plugins/.keep @@ -0,0 +1 @@ +keep this so we can install plugins into this folder diff --git a/pokemongo_bot/test/plugin_loader_test.py b/pokemongo_bot/test/plugin_loader_test.py index 4d4d5ca952..ed285ede67 100644 --- a/pokemongo_bot/test/plugin_loader_test.py +++ b/pokemongo_bot/test/plugin_loader_test.py @@ -4,25 +4,145 @@ import importlib import unittest import os +import shutil +import mock from datetime import timedelta, datetime from mock import patch, MagicMock -from pokemongo_bot.plugin_loader import PluginLoader +from pokemongo_bot.plugin_loader import PluginLoader, GithubPlugin from pokemongo_bot.test.resources.plugin_fixture import FakeTask +PLUGIN_PATH = os.path.realpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'plugins')) + class PluginLoaderTest(unittest.TestCase): def setUp(self): self.plugin_loader = PluginLoader() def test_load_namespace_class(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') - self.plugin_loader.load_path(package_path) + self.plugin_loader.load_plugin(package_path) loaded_class = self.plugin_loader.get_class('plugin_fixture.FakeTask') self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') self.plugin_loader.remove_path(package_path) def test_load_zip(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture_test.zip') - self.plugin_loader.load_path(package_path) + self.plugin_loader.load_plugin(package_path) loaded_class = self.plugin_loader.get_class('plugin_fixture_test.FakeTask') - self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTaskZip') self.plugin_loader.remove_path(package_path) + + def copy_plugin(self): + package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') + dest_path = os.path.join(PLUGIN_PATH, 'org_repo', 'plugin_fixture_tests') + shutil.copytree(package_path, os.path.join(dest_path)) + with open(os.path.join(os.path.dirname(dest_path), '.sha'), 'w') as file: + file.write('testsha') + return dest_path + + def test_load_github_already_downloaded(self): + dest_path = self.copy_plugin() + self.plugin_loader.load_plugin('org/repo#testsha') + loaded_class = self.plugin_loader.get_class('plugin_fixture_tests.FakeTask') + self.assertEqual(loaded_class({}, {}).work(), 'FakeTask') + self.plugin_loader.remove_path(dest_path) + shutil.rmtree(os.path.dirname(dest_path)) + + def copy_zip(self): + zip_name = 'test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip' + fixture_zip = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', zip_name) + zip_dest = os.path.join(PLUGIN_PATH, 'org_test-pgo-plugin_2d54eddde33061be9b329efae0cfb9bd58842655.zip') + shutil.copyfile(fixture_zip, zip_dest) + + @mock.patch.object(GithubPlugin, 'download', copy_zip) + def test_load_github_not_downloaded(self): + self.plugin_loader.load_plugin('org/test-pgo-plugin#2d54eddde33061be9b329efae0cfb9bd58842655') + loaded_class = self.plugin_loader.get_class('test-pgo-plugin.PrintText') + self.assertEqual(loaded_class({}, {}).work(), 'PrintText') + dest_path = os.path.join(PLUGIN_PATH, 'org_test-pgo-plugin') + self.plugin_loader.remove_path(os.path.join(dest_path, 'test-pgo-plugin')) + shutil.rmtree(dest_path) + +class GithubPluginTest(unittest.TestCase): + def test_get_github_parts_for_valid_github(self): + github_plugin = GithubPlugin('org/repo#sha') + self.assertTrue(github_plugin.is_valid_plugin()) + self.assertEqual(github_plugin.plugin_parts['user'], 'org') + self.assertEqual(github_plugin.plugin_parts['repo'], 'repo') + self.assertEqual(github_plugin.plugin_parts['sha'], 'sha') + + def test_get_github_parts_for_invalid_github(self): + self.assertFalse(GithubPlugin('org/repo').is_valid_plugin()) + self.assertFalse(GithubPlugin('foo').is_valid_plugin()) + self.assertFalse(GithubPlugin('/Users/foo/bar.zip').is_valid_plugin()) + + def test_get_installed_version(self): + github_plugin = GithubPlugin('org/repo#my-version') + src_fixture = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_sha') + dest = github_plugin.get_plugin_folder() + shutil.copytree(src_fixture, dest) + actual = github_plugin.get_installed_version() + shutil.rmtree(dest) + self.assertEqual('my-version', actual) + + def test_get_plugin_folder(self): + github_plugin = GithubPlugin('org/repo#sha') + expected = os.path.join(PLUGIN_PATH, 'org_repo') + actual = github_plugin.get_plugin_folder() + self.assertEqual(actual, expected) + + def test_get_local_destination(self): + github_plugin = GithubPlugin('org/repo#sha') + path = github_plugin.get_local_destination() + expected = os.path.join(PLUGIN_PATH, 'org_repo_sha.zip') + self.assertEqual(path, expected) + + def test_get_github_download_url(self): + github_plugin = GithubPlugin('org/repo#sha') + url = github_plugin.get_github_download_url() + expected = 'https://github.com/org/repo/archive/sha.zip' + self.assertEqual(url, expected) + + def test_is_already_installed_not_installed(self): + github_plugin = GithubPlugin('org/repo#sha') + self.assertFalse(github_plugin.is_already_installed()) + + def test_is_already_installed_version_mismatch(self): + github_plugin = GithubPlugin('org/repo#sha') + plugin_folder = github_plugin.get_plugin_folder() + os.mkdir(plugin_folder) + with open(os.path.join(plugin_folder, '.sha'), 'w') as file: + file.write('sha2') + + actual = github_plugin.is_already_installed() + shutil.rmtree(plugin_folder) + self.assertFalse(actual) + + def test_is_already_installed_installed(self): + github_plugin = GithubPlugin('org/repo#sha') + plugin_folder = github_plugin.get_plugin_folder() + os.mkdir(plugin_folder) + with open(os.path.join(plugin_folder, '.sha'), 'w') as file: + file.write('sha') + + actual = github_plugin.is_already_installed() + shutil.rmtree(plugin_folder) + self.assertTrue(actual) + + def test_extract(self): + github_plugin = GithubPlugin('org/test-pgo-plugin#2d54eddde33061be9b329efae0cfb9bd58842655') + src = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip') + zip_dest = github_plugin.get_local_destination() + shutil.copyfile(src, zip_dest) + github_plugin.extract() + plugin_folder = github_plugin.get_plugin_folder() + os.path.isdir(plugin_folder) + sub_folder = os.path.join(plugin_folder, 'test-pgo-plugin') + os.path.isdir(sub_folder) + sha_file = os.path.join(github_plugin.get_plugin_folder(), '.sha') + os.path.isfile(sha_file) + + with open(sha_file) as file: + content = file.read().strip() + self.assertEqual(content, '2d54eddde33061be9b329efae0cfb9bd58842655') + + shutil.rmtree(plugin_folder) diff --git a/pokemongo_bot/test/resources/plugin_fixture_test.zip b/pokemongo_bot/test/resources/plugin_fixture_test.zip index 335d95e5226056902bb02114fa26a2b3dad61ca1..78828798c32b46cb56eac6df953e7add47c175cf 100644 GIT binary patch delta 564 zcmca2HJP6$z?+$civa`#w@>8Jl2Daj5iSJ8Ul~OhKyo4s96%_teWIT+y8uvJTzBe z?UTP2rKskudemEWZEnUbYd?z(IW;{;`c(fVsP44FwpL~y9Tq!hyg8^X%D;p?TfTDjV69dB$77z~reUPTy delta 1852 zcmbQte?^KXz?+$civa|hB`5M|Ni>Nq4=?3h9{!b4gaITc!oUH9*^(3ejM+PYa-N*a zCw5$9`lT>Ano(W?Bm%-P)ew__l9L#XF(hF|O}@)$2QzE35>pp*wb=5>>zSgNnk6Um zFsn_T%fwxu92&vH02GJ2C3J$f7mJ}loA%u%egPMortW(gdJ2p5N|tt5b!_I$Ek7$@ zeOEL{^n!4u1OI}lnT|R^@;_Jqvw6M#@7~*Q&p+p@TYPEHg(u0b>km6HNbu_L&RC?) zA=+cAlYQ0H#w!PKT7Ud{i%gXhJ}3-CneNN*1o)(D|TvK($X^Z zsA>)K3CgbjKJPaBwKKXb`+@uq*F^E2PF=SA1-Y9&tvsxub#AZ$Ld+zUtRsO=&js}f~1>2B)a8U!`+N;<}#bJOulBY_=Y|AH+GN< zjqOD|Yk@AD1jLBM(JaaC2^4?>MFxS%A6Y~vvGZU?Ha(dCfU_y# zJCoA3DAyxqDr=MKrkUKFGGmGKrsymAd%M}cFj#HQ?2cRDU3TL3+{*mty}xU3RNwet z<(05E=FapTUp3oA?AcTujw&P_2x3=h?s0r|Nc{l&f#N3)w;C_pKK$hmd;Rp|AI{4p zNF0muOOA~w5Ed89Y1PQ}a5^rbB4B??P*(ZfiB^R#GD%C%w5;{ch!2cpfAjd(oI;1^ zav!(-Dp{R6bE~^ZNlWATEjcFBQ_sD2+k4_j#=F%{VOf={&$XTUu*;QO^;t*Hl{WLB z)uEF;&b}_%dSbfL>HB9^O|&^YujSyf`d?pnht`DpUR2dRUiPBYER17gkG5vZ%gBJ0 zC80t#juPi;>VXr zkcQ=D^yt!oMAu{i7IDn@IvR8v9$yOkIO-e)-u!FmSSu#Dk=Jwejp?en+f>a?>fPjh zwfSGS{0sIMXSRiQ*}a}*`=rIP_?giE+U=#A|CX7zUj6uS-a}J&4oy3rqK2k}GZ?*X zC-9{41j)xS#xcA*F1LiYqgj{de7)TV7M}ZW_?*lQi)PGdz3XnI<-2z4sx>~=R*TQa zPg8j@=_z9qOOaEu*O?QkZ=6Ng<({hFm@s$p8pd^JQhn~TvVIWeS;l5(w8<#undJ4U zRTF}Ydi_P#q@3e)-CQ>TgPkuMlHMQZ^MeEQ*)o>`xm_S6ASG0`%&M$pmM&Bdd;b8 zr*C|=V+6&S>c+C#5MZ2r1IHO7lPEJ60~;t00ZYM4EEZz0f;hk%BnK}$6<8g`pyhBl zL<(N!mb2=Dr9jeXQoEqK5K?dh1=)Pyg>ed7kP@uKLADo^WDup(Z@U diff --git a/pokemongo_bot/test/resources/plugin_sha/.sha b/pokemongo_bot/test/resources/plugin_sha/.sha new file mode 100644 index 0000000000..eaf604c9ac --- /dev/null +++ b/pokemongo_bot/test/resources/plugin_sha/.sha @@ -0,0 +1 @@ +my-version diff --git a/pokemongo_bot/test/resources/test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip b/pokemongo_bot/test/resources/test-pgo-plugin-2d54eddde33061be9b329efae0cfb9bd58842655.zip new file mode 100644 index 0000000000000000000000000000000000000000..a692ac3f08492882a28820569a62b59d2647006f GIT binary patch literal 1734 zcmWIWW@h1H00EUv?4Do-l(1yrWGG23F3~MW&(|%;DNWDJ(=|#lHAziLNl7&}HZU_x zO0`TfHnL1jOH4ILPD`>(N-?#tFflSSHPsIdVP#;vD!L*(0BSx5*!)Ruz2?453=EZY+*mHVk{C#!H>m@IgCcmF3;%OWIa(3v7jeA_4XI_~5Oe3#w|LWT6`t|M)BHw;5 z+n=YH5|>_-JvUN%$C}s*HSH>gtm6wN z>+z<~X`LdrR!?om#7$FL-8LR)Hxx0cST=G(^n!Ao*PN!P%_x4c&Yp4}6yT<(=;iumAbz&AQdA zZ&miDu6pyqY{&0vBgJIj^}ix^Xmn;rZ$Gc*vu3l*@sG+0XBOK;KmNYMRbe{M*{KKg z1#UkS3ZHKp;j~)+igT;V&YQQKUzpGKiP7JfyDm@l(VIh&{5k#HtSfmP)6B|$hs%cL z-Vu>b?(h`$WPNXT_7%I>tC{=(VKL78kN18PkUSq&-?GE$%Bs6dE+vRQovzVx^zsr> zAwwqCvnDcs)l9Y;wr1$LcuEumYKHRdP@m2srF(DXKlAzWbtShww4<&X7K&a;&784I zXnjTJpHmb62~TH;U-XXwHQCl5-?K;#m@VXi*oBg08y}yUmst`YuUAkBPq*IMr_P6- zSlgqoaq5hA;E6N3C-k)fIT#9^g0#JMpgF|$*O_^9fsQy&sY7xr;|q#1^Gc8%;@jiT z-Q*y0&rU{@JX(=-uSI>R&%z*2v5=-aj+ac?0i)BdZKc zIh0>MFi`i2ev^10>}y8W6xK`YPRyM&Ww+R@ItTB9c}#|t$4=>$^X Date: Mon, 8 Aug 2016 14:18:17 -0700 Subject: [PATCH 031/143] Refactor catch worker (#2527) * refactor catch worker * fix * few renames * add to contributors * fix * add missing behavior * fix encounter events * don't make events about ignored pokemon --- CONTRIBUTORS.md | 1 + pokemongo_bot/__init__.py | 9 +- .../cell_workers/pokemon_catch_worker.py | 799 ++++++++---------- 3 files changed, 357 insertions(+), 452 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 6dceaf0918..02aaacab4f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -51,6 +51,7 @@ * matheussampaio * Abraxas000 * lucasfevi + * pokepal * Moonlight-Angel * mjmadsen * nikofil diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 8e9a28ce3a..4ea6ac3b15 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -232,10 +232,12 @@ def _register_events(self): 'iv_display', ) ) + self.event_manager.register_event('no_pokeballs') self.event_manager.register_event( 'pokemon_catch_rate', parameters=( 'catch_rate', + 'ball_name', 'berry_name', 'berry_count' ) @@ -244,25 +246,28 @@ def _register_events(self): 'threw_berry', parameters=( 'berry_name', + 'ball_name', 'new_catch_rate' ) ) self.event_manager.register_event( 'threw_pokeball', parameters=( - 'pokeball', + 'ball_name', 'success_percentage', 'count_left' ) ) self.event_manager.register_event( - 'pokemon_escaped', + 'pokemon_capture_failed', parameters=('pokemon',) ) self.event_manager.register_event( 'pokemon_vanished', parameters=('pokemon',) ) + self.event_manager.register_event('pokemon_not_in_range') + self.event_manager.register_event('pokemon_inventory_full') self.event_manager.register_event( 'pokemon_caught', parameters=( diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index d5118b391b..a0711b49d6 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -1,13 +1,49 @@ # -*- coding: utf-8 -*- import time -from pokemongo_bot.human_behaviour import (normalized_reticle_size, sleep, - spin_modifier) from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.human_behaviour import normalized_reticle_size, sleep, spin_modifier + + +CATCH_STATUS_SUCCESS = 1 +CATCH_STATUS_FAILED = 2 +CATCH_STATUS_VANISHED = 3 + +ENCOUNTER_STATUS_SUCCESS = 1 +ENCOUNTER_STATUS_NOT_IN_RANGE = 5 +ENCOUNTER_STATUS_POKEMON_INVENTORY_FULL = 7 + +ITEM_POKEBALL = 1 +ITEM_GREATBALL = 2 +ITEM_ULTRABALL = 3 +ITEM_RAZZBERRY = 701 + +LOGIC_TO_FUNCTION = { + 'or': lambda x, y: x or y, + 'and': lambda x, y: x and y +} + + +class Pokemon(object): + + def __init__(self, pokemon_list, pokemon_data): + self.num = int(pokemon_data['pokemon_id']) - 1 + self.name = pokemon_list[int(self.num)]['Name'] + self.cp = pokemon_data['cp'] + self.attack = pokemon_data.get('individual_attack', 0) + self.defense = pokemon_data.get('individual_defense', 0) + self.stamina = pokemon_data.get('individual_stamina', 0) + + @property + def iv(self): + return round((self.attack + self.defense + self.stamina) / 45.0, 2) + + @property + def iv_display(self): + return '{}/{}/{}'.format(self.attack, self.defense, self.stamina) + class PokemonCatchWorker(BaseTask): - BAG_FULL = 'bag_full' - NO_POKEBALLS = 'no_pokeballs' def __init__(self, pokemon, bot): self.pokemon = pokemon @@ -22,444 +58,63 @@ def __init__(self, pokemon, bot): self.response_key = '' self.response_status_key = '' + ############################################################################ + # public methods + ############################################################################ + def work(self, response_dict=None): - encounter_id = self.pokemon['encounter_id'] + response_dict = response_dict or self.create_encounter_api_call() + # validate response if not response_dict: - response_dict = self.create_encounter_api_call() - - if response_dict and 'responses' in response_dict: - if self.response_key in response_dict['responses']: - if self.response_status_key in response_dict['responses'][self.response_key]: - if response_dict['responses'][self.response_key][self.response_status_key] is 1: - cp = 0 - if 'wild_pokemon' in response_dict['responses'][self.response_key] or 'pokemon_data' in \ - response_dict['responses'][self.response_key]: - if self.response_key == 'ENCOUNTER': - pokemon = response_dict['responses'][self.response_key]['wild_pokemon'] - else: - pokemon = response_dict['responses'][self.response_key] - - catch_rate = response_dict['responses'][self.response_key]['capture_probability'][ - 'capture_probability'] # 0 = pokeballs, 1 great balls, 3 ultra balls - - if 'pokemon_data' in pokemon and 'cp' in pokemon['pokemon_data']: - pokemon_data = pokemon['pokemon_data'] - cp = pokemon_data['cp'] - - individual_attack = pokemon_data.get("individual_attack", 0) - individual_stamina = pokemon_data.get("individual_stamina", 0) - individual_defense = pokemon_data.get("individual_defense", 0) - - iv_display = '{}/{}/{}'.format( - individual_attack, - individual_defense, - individual_stamina - ) - - pokemon_potential = self.pokemon_potential(pokemon_data) - pokemon_num = int(pokemon_data['pokemon_id']) - 1 - pokemon_name = self.pokemon_list[int(pokemon_num)]['Name'] - - msg = 'A wild {pokemon} appeared! [CP {cp}] [Potential {iv}] [S/A/D {iv_display}]' - self.emit_event( - 'pokemon_appeared', - formatted=msg, - data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': pokemon_potential, - 'iv_display': iv_display, - } - ) - - pokemon_data['name'] = pokemon_name - # Simulate app - sleep(3) - - if not self.should_capture_pokemon(pokemon_name, cp, pokemon_potential, response_dict): - return False - - flag_VIP = False - # @TODO, use the best ball in stock to catch VIP (Very Important Pokemon: Configurable) - if self.check_vip_pokemon(pokemon_name, cp, pokemon_potential): - self.emit_event( - 'vip_pokemon', - formatted='This is a VIP pokemon. Catch!!!' - ) - flag_VIP=True - - items_stock = self.bot.current_inventory() - berry_id = 701 # @ TODO: use better berries if possible - berries_count = self.bot.item_inventory_count(berry_id) - while True: - # pick the most simple ball from stock - pokeball = 1 # start from 1 - PokeBalls - berry_used = False - - if flag_VIP: - if(berries_count>0 and catch_rate[pokeball-1] < 0.9): - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - self.emit_event( - 'pokemon_catch_rate', - level='debug', - formatted="Catch rate of {catch_rate} is low. Maybe will throw {berry_name} ({berry_count} left)", - data={ - 'catch_rate': success_percentage, - 'berry_name': self.item_list[str(berry_id)], - 'berry_count': berries_count - } - ) - # Out of all pokeballs! Let's don't waste berry. - if items_stock[1] == 0 and items_stock[2] == 0 and items_stock[3] == 0: - break - - # Use the berry to catch - response_dict = self.api.use_item_capture( - item_id=berry_id, - encounter_id=encounter_id, - spawn_point_id=self.spawn_point_guid - ) - if response_dict and response_dict['status_code'] is 1 and 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: - for i in range(len(catch_rate)): - if 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: - catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - berries_count = berries_count -1 - berry_used = True - self.emit_event( - 'threw_berry', - formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", - data={ - "berry_name": self.item_list[str(berry_id)], - "new_catch_rate": success_percentage - } - ) - else: - if response_dict['status_code'] is 1: - self.emit_event( - 'softban', - level='warning', - formatted='Failed to use berry. You may be softbanned.' - ) - else: - self.emit_event( - 'threw_berry_failed', - formatted='Unknown response when throwing berry: {status_code}.', - data={ - 'status_code': response_dict['status_code'] - } - ) - - #use the best ball to catch - current_type = pokeball - #debug use normal ball - while current_type < 3: - current_type += 1 - if catch_rate[pokeball-1] < 0.9 and items_stock[current_type] > 0: - # if current ball chance to catch is under 90%, and player has better ball - then use it - pokeball = current_type # use better ball - else: - # If we have a lot of berries (than the great ball), we prefer use a berry first! - if catch_rate[pokeball-1] < 0.42 and items_stock[pokeball+1]+30 < berries_count: - # If it's not the VIP type, we don't want to waste our ultra ball if no balls left. - if items_stock[1] == 0 and items_stock[2] == 0: - break - - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - self.emit_event( - 'pokemon_catch_rate', - level='debug', - formatted="Catch rate of {catch_rate} is low. Maybe will throw {berry_name} ({berry_count} left)", - data={ - 'catch_rate': success_percentage, - 'berry_name': self.item_list[str(berry_id)], - 'berry_count': berries_count-1 - } - ) - response_dict = self.api.use_item_capture(item_id=berry_id, - encounter_id=encounter_id, - spawn_point_id=self.spawn_point_guid - ) - if response_dict and response_dict['status_code'] is 1 and 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: - for i in range(len(catch_rate)): - if 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: - catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - berries_count = berries_count -1 - berry_used = True - self.emit_event( - 'threw_berry', - formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", - data={ - "berry_name": self.item_list[str(berry_id)], - "new_catch_rate": success_percentage - } - ) - else: - if response_dict['status_code'] is 1: - self.emit_event( - 'softban', - level='warning', - formatted='Failed to use berry. You may be softbanned.' - ) - else: - self.emit_event( - 'threw_berry_failed', - formatted='Unknown response when throwing berry: {status_code}.', - data={ - 'status_code': response_dict['status_code'] - } - ) - - else: - #We don't have many berry to waste, pick a good ball first. Save some berry for future VIP pokemon - current_type = pokeball - while current_type < 2: - current_type += 1 - if catch_rate[pokeball-1] < 0.35 and items_stock[current_type] > 0: - # if current ball chance to catch is under 35%, and player has better ball - then use it - pokeball = current_type # use better ball - - #if the rate is still low and we didn't throw a berry before use berry - if catch_rate[pokeball-1] < 0.35 and berries_count > 0 and berry_used == False: - # If it's not the VIP type, we don't want to waste our ultra ball if no balls left. - if items_stock[1] == 0 and items_stock[2] == 0: - break - - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - self.emit_event( - 'pokemon_catch_rate', - level='debug', - formatted="Catch rate of {catch_rate} is low. Throwing {berry_name} ({berry_count} left)", - data={ - 'catch_rate': success_percentage, - 'berry_name': self.item_list[str(berry_id)], - 'berry_count': berries_count-1 - } - ) - response_dict = self.api.use_item_capture(item_id=berry_id, - encounter_id=encounter_id, - spawn_point_id=self.spawn_point_guid - ) - if response_dict and response_dict['status_code'] is 1 and 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: - for i in range(len(catch_rate)): - if 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: - catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - berries_count = berries_count -1 - berry_used = True - self.emit_event( - 'threw_berry', - formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", - data={ - "berry_name": self.item_list[str(berry_id)], - "new_catch_rate": success_percentage - } - ) - else: - if response_dict['status_code'] is 1: - self.emit_event( - 'softban', - level='warning', - formatted='Failed to use berry. You may be softbanned.' - ) - else: - self.emit_event( - 'threw_berry_failed', - formatted='Unknown response when throwing berry: {status_code}.', - data={ - 'status_code': response_dict['status_code'] - } - ) - - # Re-check if berry is used, find a ball for a good capture rate - current_type=pokeball - while current_type < 2: - current_type += 1 - if catch_rate[pokeball-1] < 0.35 and items_stock[current_type] > 0: - pokeball = current_type # use better ball - - # This is to avoid rare case that a berry has ben throwed <0.42 - # and still picking normal pokeball (out of stock) -> error - if items_stock[1] == 0 and items_stock[2] > 0: - pokeball = 2 - - # Add this logic to avoid Pokeball = 0, Great Ball = 0, Ultra Ball = X - # And this logic saves Ultra Balls if it's a weak trash pokemon - if catch_rate[pokeball-1]<0.30 and items_stock[3]>0: - pokeball = 3 - - items_stock[pokeball] -= 1 - success_percentage = '{0:.2f}'.format(catch_rate[pokeball - 1] * 100) - self.emit_event( - 'threw_pokeball', - formatted='Used {pokeball}, with chance {success_percentage} ({count_left} left)', - data={ - 'pokeball': self.item_list[str(pokeball)], - 'success_percentage': success_percentage, - 'count_left': items_stock[pokeball] - } - ) - id_list1 = self.count_pokemon_inventory() - - reticle_size_parameter = normalized_reticle_size(self.config.catch_randomize_reticle_factor) - spin_modifier_parameter = spin_modifier(self.config.catch_randomize_spin_factor) - - response_dict = self.api.catch_pokemon( - encounter_id=encounter_id, - pokeball=pokeball, - normalized_reticle_size=reticle_size_parameter, - spawn_point_id=self.spawn_point_guid, - hit_pokemon=1, - spin_modifier=spin_modifier_parameter, - normalized_hit_position=1 - ) - - if response_dict and \ - 'responses' in response_dict and \ - 'CATCH_POKEMON' in response_dict['responses'] and \ - 'status' in response_dict['responses']['CATCH_POKEMON']: - status = response_dict['responses'][ - 'CATCH_POKEMON']['status'] - if status is 2: - self.emit_event( - 'pokemon_escaped', - formatted="{pokemon} escaped.", - data={'pokemon': pokemon_name} - ) - sleep(2) - continue - if status is 3: - self.emit_event( - 'pokemon_vanished', - formatted="{pokemon} vanished!", - data={'pokemon': pokemon_name} - ) - if success_percentage == 100: - self.softban = True - if status is 1: - self.bot.metrics.captured_pokemon(pokemon_name, cp, iv_display, pokemon_potential) - - self.emit_event( - 'pokemon_caught', - formatted='Captured {pokemon}! [CP {cp}] [Potential {iv}] [{iv_display}] [+{exp} exp]', - data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': pokemon_potential, - 'iv_display': iv_display, - 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) - } - ) - self.bot.softban = False - - if (self.config.evolve_captured - and (self.config.evolve_captured[0] == 'all' - or pokemon_name in self.config.evolve_captured)): - id_list2 = self.count_pokemon_inventory() - # No need to capture this even for metrics, player stats includes it. - pokemon_to_transfer = list(set(id_list2) - set(id_list1)) - - # TODO dont throw RuntimeError, do something better - if len(pokemon_to_transfer) == 0: - raise RuntimeError( - 'Trying to evolve 0 pokemons!') - response_dict = self.api.evolve_pokemon(pokemon_id=pokemon_to_transfer[0]) - status = response_dict['responses']['EVOLVE_POKEMON']['result'] - if status == 1: - self.emit_event( - 'pokemon_evolved', - formatted="{pokemon} evolved!", - data={'pokemon': pokemon_name} - ) - else: - self.emit_event( - 'pokemon_evolve_fail', - formatted="Failed to evolve {pokemon}!", - data={'pokemon': pokemon_name} - ) - break - time.sleep(5) - - def count_pokemon_inventory(self): - # don't use cached bot.get_inventory() here - # because we need to have actual information in capture logic - response_dict = self.api.get_inventory() - - id_list = [] - callback = lambda pokemon: id_list.append(pokemon['id']) - self._foreach_pokemon_in_inventory(response_dict, callback) - return id_list - - def _foreach_pokemon_in_inventory(self, response_dict, callback): + return False try: - reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) + responses = response_dict['responses'] + response = responses[self.response_key] + if response[self.response_status_key] != ENCOUNTER_STATUS_SUCCESS: + if response[self.response_status_key] == ENCOUNTER_STATUS_NOT_IN_RANGE: + self.emit_event('pokemon_not_in_range', formatted='Pokemon went out of range!') + elif response[self.response_status_key] == ENCOUNTER_STATUS_POKEMON_INVENTORY_FULL: + self.emit_event('pokemon_inventory_full', formatted='Your Pokemon inventory is full! Could not catch!') + return False except KeyError: - pass - else: - for item in response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data"], item) - except KeyError: - pass - else: - pokemon = item['inventory_item_data']['pokemon_data'] - if not pokemon.get('is_egg', False): - callback(pokemon) - - def pokemon_potential(self, pokemon_data): - total_iv = 0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - - for individual_stat in iv_stats: - try: - total_iv += pokemon_data[individual_stat] - except: - pokemon_data[individual_stat] = 0 - continue - - return round((total_iv / 45.0), 2) - - def should_capture_pokemon(self, pokemon_name, cp, iv, response_dict): - catch_config = self._get_catch_config_for(pokemon_name) - cp_iv_logic = catch_config.get('logic') - if not cp_iv_logic: - cp_iv_logic = self._get_catch_config_for('any').get('logic', 'and') - - catch_results = { - 'cp': False, - 'iv': False, - } - - if catch_config.get('never_catch', False): return False - if catch_config.get('always_catch', False): - return True - - catch_cp = catch_config.get('catch_above_cp', 0) - if cp > catch_cp: - catch_results['cp'] = True + # get pokemon data + pokemon_data = response['wild_pokemon']['pokemon_data'] if 'wild_pokemon' in response else response['pokemon_data'] + pokemon = Pokemon(self.pokemon_list, pokemon_data) - catch_iv = catch_config.get('catch_above_iv', 0) - if iv > catch_iv: - catch_results['iv'] = True - - logic_to_function = { - 'or': lambda x, y: x or y, - 'and': lambda x, y: x and y - } + # skip ignored pokemon + if not self._should_catch_pokemon(pokemon): + return False - return logic_to_function[cp_iv_logic](*catch_results.values()) + # log encounter + self.emit_event( + 'pokemon_appeared', + formatted='A wild {pokemon} appeared! [CP {cp}] [Potential {iv}] [A/D/S {iv_display}]', + data={ + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv, + 'iv_display': pokemon.iv_display, + } + ) + + # simulate app + sleep(3) + + # check for VIP pokemon + is_vip = self._is_vip_pokemon(pokemon) + if is_vip: + self.emit_event('vip_pokemon', formatted='This is a VIP pokemon. Catch!!!') + + # catch that pokemon! + encounter_id = self.pokemon['encounter_id'] + catch_rate_by_ball = [0] + response['capture_probability']['capture_probability'] # offset so item ids match indces + self._do_catch(pokemon, encounter_id, catch_rate_by_ball, is_vip=is_vip) - def _get_catch_config_for(self, pokemon): - catch_config = self.config.catch.get(pokemon) - if not catch_config: - catch_config = self.config.catch.get('any') - return catch_config + # simulate app + time.sleep(5) def create_encounter_api_call(self): encounter_id = self.pokemon['encounter_id'] @@ -491,29 +146,273 @@ def create_encounter_api_call(self): ) return request.call() - def check_vip_pokemon(self,pokemon, cp, iv): + ############################################################################ + # helpers + ############################################################################ + + def _pokemon_matches_config(self, config, pokemon, default_logic='and'): + pokemon_config = config.get(pokemon.name, config.get('any')) + + if not pokemon_config: + return False - vip_name = self.config.vips.get(pokemon) - if vip_name == {}: - return True - else: - catch_config = self.config.vips.get("any") - if not catch_config: - return False - cp_iv_logic = catch_config.get('logic', 'or') catch_results = { 'cp': False, 'iv': False, } - catch_cp = catch_config.get('catch_above_cp', 0) - if cp > catch_cp: + if pokemon_config.get('never_catch', False): + return False + + if pokemon_config.get('always_catch', False): + return True + + catch_cp = pokemon_config.get('catch_above_cp', 0) + if pokemon.cp > catch_cp: catch_results['cp'] = True - catch_iv = catch_config.get('catch_above_iv', 0) - if iv > catch_iv: + + catch_iv = pokemon_config.get('catch_above_iv', 0) + if pokemon.iv > catch_iv: catch_results['iv'] = True - logic_to_function = { - 'or': lambda x, y: x or y, - 'and': lambda x, y: x and y - } - return logic_to_function[cp_iv_logic](*catch_results.values()) + + return LOGIC_TO_FUNCTION[pokemon_config.get('logic', default_logic)](*catch_results.values()) + + def _should_catch_pokemon(self, pokemon): + return self._pokemon_matches_config(self.config.catch, pokemon) + + def _is_vip_pokemon(self, pokemon): + # having just a name present in the list makes them vip + if self.config.vips.get(pokemon.name) == {}: + return True + return self._pokemon_matches_config(self.config.vips, pokemon, default_logic='or') + + def _get_current_pokemon_ids(self): + # don't use cached bot.get_inventory() here because we need to have actual information in capture logic + response_dict = self.api.get_inventory() + + try: + inventory_items = response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] + except KeyError: + return [] # no items + + id_list = [] + for item in inventory_items: + try: + pokemon = item['inventory_item_data']['pokemon_data'] + except KeyError: + continue + + # ignore eggs + if pokemon.get('is_egg'): + continue + + id_list.append(pokemon['id']) + + return id_list + + def _pct(self, rate_by_ball): + return '{0:.2f}'.format(rate_by_ball * 100) + + def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball): + new_catch_rate_by_ball = [] + self.emit_event( + 'pokemon_catch_rate', + level='debug', + formatted='Catch rate of {catch_rate} with {ball_name} is low. Throwing {berry_name} (have {berry_count})', + data={ + 'catch_rate': self._pct(catch_rate_by_ball[current_ball]), + 'ball_name': self.item_list[str(current_ball)], + 'berry_name': self.item_list[str(berry_id)], + 'berry_count': berry_count + } + ) + + response_dict = self.api.use_item_capture( + item_id=berry_id, + encounter_id=encounter_id, + spawn_point_id=self.spawn_point_guid + ) + responses = response_dict['responses'] + + if response_dict and response_dict['status_code'] == 1: + + # update catch rates using multiplier + if 'item_capture_mult' in responses['USE_ITEM_CAPTURE']: + for rate in catch_rate_by_ball: + new_catch_rate_by_ball.append(rate * responses['USE_ITEM_CAPTURE']['item_capture_mult']) + self.emit_event( + 'threw_berry', + formatted="Threw a {berry_name}! Catch rate with {ball_name} is now: {new_catch_rate}", + data={ + 'berry_name': self.item_list[str(berry_id)], + 'ball_name': self.item_list[str(current_ball)], + 'new_catch_rate': self._pct(catch_rate_by_ball[current_ball]) + } + ) + + # softban? + else: + self.emit_event( + 'softban', + level='warning', + formatted='Failed to use berry. You may be softbanned.' + ) + + # unknown status code + else: + self.emit_event( + 'threw_berry_failed', + formatted='Unknown response when throwing berry: {status_code}.', + data={ + 'status_code': response_dict['status_code'] + } + ) + + return new_catch_rate_by_ball + + def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): + # settings that may be exposed at some point + berry_id = ITEM_RAZZBERRY + maximum_ball = ITEM_ULTRABALL if is_vip else ITEM_GREATBALL + ideal_catch_rate_before_throw = 0.9 if is_vip else 0.35 + + berry_count = self.bot.item_inventory_count(berry_id) + items_stock = self.bot.current_inventory() + + while True: + + # find lowest available ball + current_ball = ITEM_POKEBALL + while items_stock[current_ball] == 0 and current_ball < maximum_ball: + current_ball += 1 + if items_stock[current_ball] == 0: + self.emit_event('no_pokeballs', formatted='No usable pokeballs found!') + break + + # check future ball count + num_next_balls = 0 + next_ball = current_ball + while next_ball < maximum_ball: + next_ball += 1 + num_next_balls += items_stock[next_ball] + + # check if we've got berries to spare + berries_to_spare = berry_count > 0 if is_vip else berry_count > num_next_balls + 30 + + # use a berry if we are under our ideal rate and have berries to spare + used_berry = False + if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and berries_to_spare: + catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) + berry_count -= 1 + used_berry = True + + # pick the best ball to catch with + best_ball = current_ball + while best_ball < maximum_ball: + best_ball += 1 + if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and items_stock[best_ball] > 0: + # if current ball chance to catch is under our ideal rate, and player has better ball - then use it + current_ball = best_ball + + # if the rate is still low and we didn't throw a berry before, throw one + if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and berry_count > 0 and not used_berry: + catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) + berry_count -= 1 + + # get current pokemon list before catch + pokemon_before_catch = self._get_current_pokemon_ids() + + # try to catch pokemon! + items_stock[current_ball] -= 1 + self.emit_event( + 'threw_pokeball', + formatted='Used {ball_name}, with chance {success_percentage} ({count_left} left)', + data={ + 'ball_name': self.item_list[str(current_ball)], + 'success_percentage': self._pct(catch_rate_by_ball[current_ball]), + 'count_left': items_stock[current_ball] + } + ) + + reticle_size_parameter = normalized_reticle_size(self.config.catch_randomize_reticle_factor) + spin_modifier_parameter = spin_modifier(self.config.catch_randomize_spin_factor) + + response_dict = self.api.catch_pokemon( + encounter_id=encounter_id, + pokeball=current_ball, + normalized_reticle_size=reticle_size_parameter, + spawn_point_id=self.spawn_point_guid, + hit_pokemon=1, + spin_modifier=spin_modifier_parameter, + normalized_hit_position=1 + ) + + try: + catch_pokemon_status = response_dict['responses']['CATCH_POKEMON']['status'] + except KeyError: + break + + # retry failed pokemon + if catch_pokemon_status == CATCH_STATUS_FAILED: + self.emit_event( + 'pokemon_capture_failed', + formatted='{pokemon} capture failed.. trying again!', + data={'pokemon': pokemon.name} + ) + sleep(2) + continue + + # abandon if pokemon vanished + elif catch_pokemon_status == CATCH_STATUS_VANISHED: + self.emit_event( + 'pokemon_vanished', + formatted='{pokemon} vanished!', + data={'pokemon': pokemon.name} + ) + if self._pct(catch_rate_by_ball[current_ball]) == 100: + self.bot.softban = True + + # pokemon caught! + elif catch_pokemon_status == CATCH_STATUS_SUCCESS: + self.bot.metrics.captured_pokemon(pokemon.name, pokemon.cp, pokemon.iv_display, pokemon.iv) + self.emit_event( + 'pokemon_caught', + formatted='Captured {pokemon}! [CP {cp}] [Potential {iv}] [{iv_display}] [+{exp} exp]', + data={ + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv, + 'iv_display': pokemon.iv_display, + 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) + } + ) + self.bot.softban = False + + # evolve pokemon if necessary + if self.config.evolve_captured and (self.config.evolve_captured[0] == 'all' or pokemon.name in self.config.evolve_captured): + pokemon_after_catch = self._get_current_pokemon_ids() + pokemon_to_evolve = list(set(pokemon_after_catch) - set(pokemon_before_catch)) + + if len(pokemon_to_evolve) == 0: + break + + self._do_evolve(pokemon, pokemon_to_evolve[0]) + + break + + def _do_evolve(self, pokemon, new_pokemon_id): + response_dict = self.api.evolve_pokemon(pokemon_id=new_pokemon_id) + catch_pokemon_status = response_dict['responses']['EVOLVE_POKEMON']['result'] + + if catch_pokemon_status == 1: + self.emit_event( + 'pokemon_evolved', + formatted='{pokemon} evolved!', + data={'pokemon': pokemon.name} + ) + else: + self.emit_event( + 'pokemon_evolve_fail', + formatted='Failed to evolve {pokemon}!', + data={'pokemon': pokemon.name} + ) From 597196e4af4484a192bdb972f0f4781ecd91731e Mon Sep 17 00:00:00 2001 From: devn0ll Date: Tue, 9 Aug 2016 02:17:23 +0200 Subject: [PATCH 032/143] Added Run-Loop (#3143) * Add files via upload modified run script wich let you run the boot in a loop(if it crashes it restarts) * Integreated Loop into run.sh modified run.sh to loop the script so that even if it crashes it automaticly restarts. --- run.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/run.sh b/run.sh index ec95acb3e3..d7ef7079b7 100755 --- a/run.sh +++ b/run.sh @@ -15,4 +15,12 @@ else fi fi +while [ true ] +do +echo "###############################################" +echo "##Exit two times with [Ctl+C] to end the loop##" +echo "###############################################" +sleep 1 python pokecli.py --config ${config} +sleep "10" +done From ec3babc8d7218b7a92913a04ced597b605365b79 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Tue, 9 Aug 2016 02:27:21 +0200 Subject: [PATCH 033/143] fixing loop in spin fort task (#3165) --- pokemongo_bot/cell_workers/spin_fort.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index e04a86dbc6..2b1c862fc9 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -139,6 +139,11 @@ def work(self): def get_fort_in_range(self): forts = self.bot.get_forts(order_by_distance=True) + for fort in forts: + if 'cooldown_complete_timestamp_ms' in fort: + self.bot.fort_timeouts[fort["id"]] = fort['cooldown_complete_timestamp_ms'] + forts.remove(fort) + forts = filter(lambda x: x["id"] not in self.bot.fort_timeouts, forts) if len(forts) == 0: From a2b9fe9be9642612420612795a15bc691189f8c9 Mon Sep 17 00:00:00 2001 From: Guru Prasad Date: Mon, 8 Aug 2016 20:28:38 -0400 Subject: [PATCH 034/143] Some love for the vim users (#3154) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 70ab34b250..4721ce0253 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,6 @@ include/ # Pip check file pip-selfcheck.json + +# Some love for the vim users +.*.sw* From 8a2a52bad0b381f265f0896e69c50566e11da67c Mon Sep 17 00:00:00 2001 From: Jack Venberg Date: Mon, 8 Aug 2016 21:18:57 -0700 Subject: [PATCH 035/143] Updated README with link to desktop version (#3208) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 171ee4f355..fd99669e74 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ PokemonGo bot is a project created by the [PokemonGoF](https://github.com/PokemonGoF) team. The project is currently setup in two main branches. `dev` and `master`. +## Help Needed on [Desktop Version](https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) + ## Please submit PR to [Dev branch](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) From 5e3179b9c22d355f5f324751d1e5d6df7c2b1148 Mon Sep 17 00:00:00 2001 From: Kraig Amador Date: Mon, 8 Aug 2016 21:20:47 -0700 Subject: [PATCH 036/143] Fix for #3190 (#3197) --- CONTRIBUTORS.md | 1 + pokemongo_bot/cell_workers/pokemon_catch_worker.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 02aaacab4f..89011f7c38 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -55,3 +55,4 @@ * Moonlight-Angel * mjmadsen * nikofil + * bigkraig diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index a0711b49d6..ae55b7d704 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -252,6 +252,7 @@ def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, cu # softban? else: + new_catch_rate_by_ball = catch_rate_by_ball self.emit_event( 'softban', level='warning', @@ -260,6 +261,7 @@ def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, cu # unknown status code else: + new_catch_rate_by_ball = catch_rate_by_ball self.emit_event( 'threw_berry_failed', formatted='Unknown response when throwing berry: {status_code}.', From e4c54dc593b7202ac95ebd70cfc672e88eb878ee Mon Sep 17 00:00:00 2001 From: pmquan Date: Tue, 9 Aug 2016 00:25:42 -0700 Subject: [PATCH 037/143] MoveToMap: Add minimum balls to run (#3166) --- configs/config.json.map.example | 1 + pokemongo_bot/cell_workers/move_to_map_pokemon.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/configs/config.json.map.example b/configs/config.json.map.example index 1079c999f9..43196196f5 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -58,6 +58,7 @@ "address": "http://localhost:5000", "max_distance": 500, "min_time": 60, + "min_ball": 50, "prioritize_vips": true, "snipe": false, "update_map": true, diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 9ab6bbb7a2..c97b240196 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -82,6 +82,7 @@ def initialize(self): self.pokemon_data = self.bot.pokemon_list self.unit = self.bot.config.distance_unit self.caught = [] + self.min_ball = self.config.get('min_ball', 1) data_file = 'data/map-caught-{}.json'.format(self.bot.config.username) if os.path.isfile(data_file): @@ -250,7 +251,7 @@ def work(self): pokemon = pokemon_list[0] # if we only have ultraballs and the target is not a vip don't snipe/walk - if (pokeballs + superballs) < 1 and not pokemon['is_vip']: + if (pokeballs + superballs) < self.min_ball and not pokemon['is_vip']: return WorkerResult.SUCCESS if self.config['snipe']: From 61b6854c8c78789c7ee3343df48cb96b25f229a9 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Tue, 9 Aug 2016 09:40:52 +0200 Subject: [PATCH 038/143] added config to ignore item count for Spin and MoveToFort (#3160) --- pokemongo_bot/cell_workers/move_to_fort.py | 9 +++++---- pokemongo_bot/cell_workers/spin_fort.py | 8 +++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index e4b4187d20..7dcd0977b1 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -13,17 +13,18 @@ class MoveToFort(BaseTask): def initialize(self): self.lure_distance = 0 - self.lure_attraction = True #self.config.get("lure_attraction", True) - self.lure_max_distance = 2000 #self.config.get("lure_max_distance", 2000) + self.lure_attraction = self.config.get("lure_attraction", True) + self.lure_max_distance = self.config.get("lure_max_distance", 2000) + self.ignore_item_count = self.config.get("ignore_item_count", False) def should_run(self): has_space_for_loot = self.bot.has_space_for_loot() if not has_space_for_loot: self.emit_event( 'inventory_full', - formatted="Not moving to any forts as there aren't enough space. You might want to change your config to recycle more items if this message appears consistently." + formatted="Inventory is full. You might want to change your config to recycle more items if this message appears consistently." ) - return has_space_for_loot or self.bot.softban + return has_space_for_loot or self.ignore_item_count or self.bot.softban def is_attracted(self): return (self.lure_distance > 0) diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 2b1c862fc9..445946e7e1 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -15,14 +15,16 @@ class SpinFort(BaseTask): SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): + self.ignore_item_count = self.config.get("ignore_item_count", False) + def should_run(self): if not self.bot.has_space_for_loot(): self.emit_event( 'inventory_full', - formatted="Not moving to any forts as there aren't enough space. You might want to change your config to recycle more items if this message appears consistently." + formatted="Inventory is full. You might want to change your config to recycle more items if this message appears consistently." ) - return False - return True + return self.ignore_item_count or self.bot.has_space_for_loot() def work(self): fort = self.get_fort_in_range() From 03d7f92ebf127572337d5fab748a4bde1577a186 Mon Sep 17 00:00:00 2001 From: aeckert Date: Tue, 9 Aug 2016 02:18:31 -0600 Subject: [PATCH 039/143] [Inventory Management] Add a central class for caching/parsing inventory & static data (#2528) * new class to centralize inventory management * use new inventory class in evolve_pokemon * use new inventory to display # candy after catch --- pokemongo_bot/__init__.py | 7 +- pokemongo_bot/cell_workers/evolve_pokemon.py | 85 ++---- .../cell_workers/pokemon_catch_worker.py | 18 +- pokemongo_bot/inventory.py | 251 ++++++++++++++++++ 4 files changed, 293 insertions(+), 68 deletions(-) create mode 100644 pokemongo_bot/inventory.py diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 4ea6ac3b15..711ddd1721 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -30,8 +30,11 @@ from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl from worker_result import WorkerResult from tree_config_builder import ConfigException, MismatchTaskApiVersion, TreeConfigBuilder +from inventory import init_inventory from sys import platform as _platform import struct + + class PokemonGoBot(object): @property def position(self): @@ -286,7 +289,7 @@ def _register_events(self): self.event_manager.register_event('skip_evolve') self.event_manager.register_event('threw_berry_failed', parameters=('status_code',)) self.event_manager.register_event('vip_pokemon') - + self.event_manager.register_event('gained_candy', parameters=('quantity', 'type')) # level up stuff self.event_manager.register_event( @@ -782,6 +785,8 @@ def get_inventory(self): return self.latest_inventory def update_inventory(self): + # TODO: transition to using this inventory class everywhere + init_inventory(self) response = self.get_inventory() self.inventory = list() inventory_items = response.get('responses', {}).get('GET_INVENTORY', {}).get( diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index c3903a685d..d045fc8fef 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -1,3 +1,4 @@ +from pokemongo_bot import inventory from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.item_list import Item from pokemongo_bot.base_task import BaseTask @@ -25,21 +26,16 @@ def work(self): if not self._should_run(): return - response_dict = self.api.get_inventory() - inventory_items = response_dict.get('responses', {}).get('GET_INVENTORY', {}).get('inventory_delta', {}).get( - 'inventory_items', {}) - - evolve_list = self._sort_and_filter(inventory_items) + evolve_list = self._sort_and_filter() if self.evolve_all[0] != 'all': # filter out non-listed pokemons - evolve_list = filter(lambda x: x["name"] in self.evolve_all, evolve_list) + evolve_list = filter(lambda x: x.name in self.evolve_all, evolve_list) cache = {} - candy_list = self._get_candy_list(inventory_items) for pokemon in evolve_list: - if self._can_evolve(pokemon, candy_list, cache): - self._execute_pokemon_evolve(pokemon, candy_list, cache) + if pokemon.can_evolve_now(): + self._execute_pokemon_evolve(pokemon, cache) def _should_run(self): if not self.evolve_all or self.evolve_all[0] == 'none': @@ -80,81 +76,40 @@ def _should_run(self): ) return False - def _get_candy_list(self, inventory_items): - candies = {} - for item in inventory_items: - candy = item.get('inventory_item_data', {}).get('candy', {}) - family_id = candy.get('family_id', 0) - amount = candy.get('candy', 0) - if family_id > 0 and amount > 0: - family = self.bot.pokemon_list[family_id - 1]['Name'] + " candies" - candies[family] = amount - - return candies - - def _sort_and_filter(self, inventory_items): + def _sort_and_filter(self): pokemons = [] logic_to_function = { - 'or': lambda pokemon: pokemon["cp"] >= self.evolve_above_cp or pokemon["iv"] >= self.evolve_above_iv, - 'and': lambda pokemon: pokemon["cp"] >= self.evolve_above_cp and pokemon["iv"] >= self.evolve_above_iv + 'or': lambda pokemon: pokemon.cp >= self.evolve_above_cp or pokemon.iv >= self.evolve_above_iv, + 'and': lambda pokemon: pokemon.cp >= self.evolve_above_cp and pokemon.iv >= self.evolve_above_iv } - for item in inventory_items: - pokemon = item.get('inventory_item_data', {}).get('pokemon_data', {}) - pokemon_num = int(pokemon.get('pokemon_id', 0)) - 1 - next_evol = self.bot.pokemon_list[pokemon_num].get('Next Evolution Requirements', {}) - pokemon = { - 'id': pokemon.get('id', 0), - 'num': pokemon_num, - 'name': self.bot.pokemon_list[pokemon_num]['Name'], - 'cp': pokemon.get('cp', 0), - 'iv': self._compute_iv(pokemon), - 'candies_family': next_evol.get('Name', ""), - 'candies_amount': next_evol.get('Amount', 0) - } - if pokemon["id"] > 0 and pokemon["candies_amount"] > 0 and (logic_to_function[self.cp_iv_logic](pokemon)): + + for pokemon in inventory.pokemons().all(): + if pokemon.id > 0 and pokemon.has_next_evolution() and (logic_to_function[self.cp_iv_logic](pokemon)): pokemons.append(pokemon) if self.first_evolve_by == "cp": - pokemons.sort(key=lambda x: (x['num'], x["cp"], x["iv"]), reverse=True) + pokemons.sort(key=lambda x: (x.pokemon_id, x.cp, x.iv), reverse=True) else: - pokemons.sort(key=lambda x: (x['num'], x["iv"], x["cp"]), reverse=True) + pokemons.sort(key=lambda x: (x.pokemon_id, x.iv, x.cp), reverse=True) return pokemons - def _can_evolve(self, pokemon, candy_list, cache): - - if pokemon["name"] in cache: - return False - - family = pokemon["candies_family"] - amount = pokemon["candies_amount"] - if family in candy_list and candy_list[family] >= amount: - return True - else: - cache[pokemon["name"]] = 1 - return False - - def _execute_pokemon_evolve(self, pokemon, candy_list, cache): - pokemon_id = pokemon["id"] - pokemon_name = pokemon["name"] - pokemon_cp = pokemon["cp"] - pokemon_iv = pokemon["iv"] - - if pokemon_name in cache: + def _execute_pokemon_evolve(self, pokemon, cache): + if pokemon.name in cache: return False - response_dict = self.api.evolve_pokemon(pokemon_id=pokemon_id) + response_dict = self.api.evolve_pokemon(pokemon_id=pokemon.id) if response_dict.get('responses', {}).get('EVOLVE_POKEMON', {}).get('result', 0) == 1: self.emit_event( 'pokemon_evolved', formatted="Successfully evolved {pokemon} with CP {cp} and IV {iv}!", data={ - 'pokemon': pokemon_name, - 'iv': pokemon_iv, - 'cp': pokemon_cp + 'pokemon': pokemon.name, + 'iv': pokemon.iv, + 'cp': pokemon.cp } ) - candy_list[pokemon["candies_family"]] -= pokemon["candies_amount"] + inventory.candies().get(pokemon.pokemon_id).consume(pokemon.evolution_cost) sleep(self.evolve_speed) return True else: diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index ae55b7d704..207ed83d81 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import time +from pokemongo_bot import inventory from pokemongo_bot.base_task import BaseTask from pokemongo_bot.human_behaviour import normalized_reticle_size, sleep, spin_modifier @@ -27,8 +28,8 @@ class Pokemon(object): def __init__(self, pokemon_list, pokemon_data): - self.num = int(pokemon_data['pokemon_id']) - 1 - self.name = pokemon_list[int(self.num)]['Name'] + self.num = int(pokemon_data['pokemon_id']) + self.name = pokemon_list[int(self.num) - 1]['Name'] self.cp = pokemon_data['cp'] self.attack = pokemon_data.get('individual_attack', 0) self.defense = pokemon_data.get('individual_defense', 0) @@ -388,6 +389,19 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) } ) + + # We could refresh here too, but adding 3 saves a inventory request + candy = inventory.candies().get(pokemon.num) + candy.add(3) + self.emit_event( + 'gained_candy', + formatted='You now have {quantity} {type} candy!', + data = { + 'quantity': candy.quantity, + 'type': candy.type, + }, + ) + self.bot.softban = False # evolve pokemon if necessary diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py new file mode 100644 index 0000000000..1cd8a893b2 --- /dev/null +++ b/pokemongo_bot/inventory.py @@ -0,0 +1,251 @@ +import json +import os + +''' +Helper class for updating/retrieving Inventory data +''' + +class _BaseInventoryComponent(object): + TYPE = None # base key name for items of this type + ID_FIELD = None # identifier field for items of this type + STATIC_DATA_FILE = None # optionally load static data from file, + # dropping the data in a static variable named STATIC_DATA + + def __init__(self): + self._data = {} + if self.STATIC_DATA_FILE is not None: + self.init_static_data() + + @classmethod + def init_static_data(cls): + if not hasattr(cls, 'STATIC_DATA') or cls.STATIC_DATA is None: + cls.STATIC_DATA = json.load(open(cls.STATIC_DATA_FILE)) + + def parse(self, item): + # optional hook for parsing the dict for this item + # default is to use the dict directly + return item + + def retrieve_data(self, inventory): + assert self.TYPE is not None + assert self.ID_FIELD is not None + ret = {} + for item in inventory: + data = item['inventory_item_data'] + if self.TYPE in data: + item = data[self.TYPE] + key = item[self.ID_FIELD] + ret[key] = self.parse(item) + return ret + + def refresh(self, inventory): + self._data = self.retrieve_data(inventory) + + def get(self, id): + return self._data(id) + + def all(self): + return list(self._data.values()) + + +class Candy(object): + def __init__(self, family_id, quantity): + self.type = Pokemons.name_for(family_id) + self.quantity = quantity + + def consume(self, amount): + if self.quantity < amount: + raise Exception('Tried to consume more {} candy than you have'.format(self.type)) + self.quantity -= amount + + def add(self, amount): + if amount < 0: + raise Exception('Must add positive amount of candy') + self.quantity += amount + +class Candies(_BaseInventoryComponent): + TYPE = 'candy' + ID_FIELD = 'family_id' + + @classmethod + def family_id_for(self, pokemon_id): + return Pokemons.first_evolution_id_for(pokemon_id) + + def get(self, pokemon_id): + family_id = self.family_id_for(pokemon_id) + return self._data.setdefault(family_id, Candy(family_id, 0)) + + def parse(self, item): + candy = item['candy'] if 'candy' in item else 0 + return Candy(item['family_id'], candy) + + +class Pokedex(_BaseInventoryComponent): + TYPE = 'pokedex_entry' + ID_FIELD = 'pokemon_id' + + def seen(self, pokemon_id): + return pokemon_id in self._data + + def captured(self, pokemon_id): + if not self.seen(pokemon_id): + return False + return self._data[pokemon_id]['times_captured'] > 0 + + +class Items(_BaseInventoryComponent): + TYPE = 'item' + ID_FIELD = 'item_id' + STATIC_DATA_FILE = os.path.join('data', 'items.json') + + def count_for(self, item_id): + return self._data[item_id]['count'] + + +class Pokemons(_BaseInventoryComponent): + TYPE = 'pokemon_data' + ID_FIELD = 'id' + STATIC_DATA_FILE = os.path.join('data', 'pokemon.json') + + def parse(self, item): + if 'is_egg' in item: + return Egg(item) + return Pokemon(item) + + @classmethod + def data_for(cls, pokemon_id): + return cls.STATIC_DATA[pokemon_id - 1] + + @classmethod + def name_for(cls, pokemon_id): + return cls.data_for(pokemon_id)['Name'] + + @classmethod + def first_evolution_id_for(cls, pokemon_id): + data = cls.data_for(pokemon_id) + if 'Previous evolution(s)' in data: + return int(data['Previous evolution(s)'][0]['Number']) + return pokemon_id + + @classmethod + def next_evolution_id_for(cls, pokemon_id): + try: + return int(cls.data_for(pokemon_id)['Next evolution(s)'][0]['Number']) + except KeyError: + return None + + @classmethod + def evolution_cost_for(cls, pokemon_id): + try: + return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + except KeyError: + return + + def all(self): + # by default don't include eggs in all pokemon (usually just + # makes caller's lives more difficult) + return [p for p in super(Pokemons, self).all() if not isinstance(p, Egg)] + +class Egg(object): + def __init__(self, data): + self._data = data + + def has_next_evolution(self): + return False + + +class Pokemon(object): + def __init__(self, data): + self._data = data + self.id = data['id'] + self.pokemon_id = data['pokemon_id'] + self.cp = data['cp'] + self._static_data = Pokemons.data_for(self.pokemon_id) + self.name = Pokemons.name_for(self.pokemon_id) + self.iv = self._compute_iv() + + def can_evolve_now(self): + return self.has_next_evolution and self.candy_quantity > self.evolution_cost + + def has_next_evolution(self): + return 'Next Evolution Requirements' in self._static_data + + def has_seen_next_evolution(self): + return pokedex().captured(self.next_evolution_id) + + @property + def next_evolution_id(self): + return Pokemons.next_evolution_id_for(self.pokemon_id) + + @property + def first_evolution_id(self): + return Pokemons.first_evolution_id_for(self.pokemon_id) + + @property + def candy_quantity(self): + return candies().get(self.pokemon_id).quantity + + @property + def evolution_cost(self): + return self._static_data['Next Evolution Requirements']['Amount'] + + def _compute_iv(self): + total_IV = 0.0 + iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] + + for individual_stat in iv_stats: + try: + total_IV += self._data[individual_stat] + except Exception: + self._data[individual_stat] = 0 + continue + pokemon_potential = round((total_IV / 45.0), 2) + return pokemon_potential + + +class Inventory(object): + def __init__(self, bot): + self.bot = bot + self.pokedex = Pokedex() + self.candy = Candies() + self.items = Items() + self.pokemons = Pokemons() + self.refresh() + + def refresh(self): + # TODO: it would be better if this class was used for all + # inventory management. For now, I'm just clearing the old inventory field + self.bot.latest_inventory = None + inventory = self.bot.get_inventory()['responses']['GET_INVENTORY'][ + 'inventory_delta']['inventory_items'] + for i in (self.pokedex, self.candy, self.items, self.pokemons): + i.refresh(inventory) + + +_inventory = None + +def init_inventory(bot): + global _inventory + _inventory = Inventory(bot) + + +def refresh_inventory(): + _inventory.refresh() + + +def pokedex(): + return _inventory.pokedex + + +def candies(refresh=False): + if refresh: + refresh_inventory() + return _inventory.candy + + +def pokemons(): + return _inventory.pokemons + + +def items(): + return _inventory.items From f6d73af7d8c664673c3b18617404204a766e82d4 Mon Sep 17 00:00:00 2001 From: Eli White Date: Tue, 9 Aug 2016 01:20:26 -0700 Subject: [PATCH 040/143] Keeping a cache of gym information (#3236) --- pokemongo_bot/__init__.py | 4 +++- pokemongo_bot/gym_cache.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 pokemongo_bot/gym_cache.py diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 711ddd1721..6f45034f1a 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -25,6 +25,7 @@ from human_behaviour import sleep from item_list import Item from metrics import Metrics +from gym_cache import GymCache from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler from pokemongo_bot.socketio_server.runner import SocketIoRunner from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl @@ -61,6 +62,7 @@ def __init__(self, config): ) self.item_list = json.load(open(os.path.join('data', 'items.json'))) self.metrics = Metrics(self) + self.gym_cache = GymCache(self) self.latest_inventory = None self.cell = None self.recent_forts = [None] * config.forts_max_circle_size @@ -499,7 +501,7 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): if 'forts' in cell: for fort in cell['forts']: if fort.get('type') != 1: - response_gym_details = self.api.get_gym_details( + response_gym_details = self.gym_cache.get( gym_id=fort.get('id'), player_latitude=lng, player_longitude=lat, diff --git a/pokemongo_bot/gym_cache.py b/pokemongo_bot/gym_cache.py new file mode 100644 index 0000000000..b7bc7eb1eb --- /dev/null +++ b/pokemongo_bot/gym_cache.py @@ -0,0 +1,32 @@ +import time + +class GymCache(object): + def __init__(self, bot): + self.bot = bot + self.cache = {} + self.cache_length_seconds = 60 * 10 + + def get(self, gym_id, player_latitude, player_longitude, gym_latitude, gym_longitude): + if gym_id not in self.cache: + response_gym_details = self.bot.api.get_gym_details( + gym_id=gym_id, + player_latitude=player_latitude, + player_longitude=player_longitude, + gym_latitude=gym_latitude, + gym_longitude=gym_longitude + ) + + self.cache[gym_id] = response_gym_details + + gym_info = self.cache[gym_id] + gym_info['last_accessed'] = time.time() + + self._remove_stale_gyms() + return gym_info + + def _remove_stale_gyms(self): + for gym_id, gym_details in self.cache.items(): + if gym_details['last_accessed'] < time.time() - self.cache_length_seconds: + del self.cache[gym_id] + + From ce9eb2b54f9c0264af07a19a6b0a379ea484c78e Mon Sep 17 00:00:00 2001 From: aceradryd Date: Tue, 9 Aug 2016 10:37:31 +0200 Subject: [PATCH 041/143] New Option: "dont_nickname_favorite" (#2496) * New Option: "dont_nickname_favorite" This change (line 19) adds the option, that the user can choose, whether their favorite pokemons should also get a new nickname or not. If a user want this, then he or she has to add the line ("dont_nickname_favorite" = true) after ("nickname_template": " ... ",). * Update nickname_pokemon.py * Update * Put change to line 30 This reduce the reduce the runtime, because favorite pokemon won't be added to the list. --- pokemongo_bot/cell_workers/nickname_pokemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py index cda206ad20..e521cadfba 100644 --- a/pokemongo_bot/cell_workers/nickname_pokemon.py +++ b/pokemongo_bot/cell_workers/nickname_pokemon.py @@ -27,7 +27,7 @@ def _get_inventory_pokemon(self,inventory_dict): except KeyError: pass else: - if not pokemon.get('is_egg',False): + if not pokemon.get('is_egg',False) and not (pokemon.get('favorite', 0) == 1 and self.config.get('dont_nickname_favorite',False)): pokemon_data.append(pokemon) return pokemon_data From 7e699dd8b0d2ddfcd4b1b9ea8757d139f20f44d0 Mon Sep 17 00:00:00 2001 From: Eli White Date: Tue, 9 Aug 2016 02:08:01 -0700 Subject: [PATCH 042/143] Restart the loop when catching pokemon and there are more to catch (#3242) --- .../cell_workers/catch_visible_pokemon.py | 28 ++++++++++++++++--- .../cell_workers/pokemon_catch_worker.py | 10 +++---- pokemongo_bot/worker_result.py | 1 + 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py index 1bfed225df..654c2467b3 100644 --- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -3,13 +3,24 @@ from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker from utils import distance +from pokemongo_bot.worker_result import WorkerResult class CatchVisiblePokemon(BaseTask): SUPPORTED_TASK_API_VERSION = 1 def work(self): - if 'catchable_pokemons' in self.bot.cell and len(self.bot.cell['catchable_pokemons']) > 0: + num_catchable_pokemon = 0 + if 'catchable_pokemons' in self.bot.cell: + num_catchable_pokemon = len(self.bot.cell['catchable_pokemons']) + + num_wild_pokemon = 0 + if 'wild_pokemons' in self.bot.cell: + num_wild_pokemon = len(self.bot.cell['wild_pokemons']) + + num_available_pokemon = num_catchable_pokemon + num_wild_pokemon + + if num_catchable_pokemon > 0: # Sort all by distance from current pos- eventually this should # build graph & A* it self.bot.cell['catchable_pokemons'].sort( @@ -33,15 +44,24 @@ def work(self): } ) - return self.catch_pokemon(self.bot.cell['catchable_pokemons'].pop(0)) + self.catch_pokemon(self.bot.cell['catchable_pokemons'].pop(0)) + if num_catchable_pokemon > 1: + return WorkerResult.RUNNING + else: + return WorkerResult.SUCCESS - if 'wild_pokemons' in self.bot.cell and len(self.bot.cell['wild_pokemons']) > 0: + if num_available_pokemon > 0: # Sort all by distance from current pos- eventually this should # build graph & A* it self.bot.cell['wild_pokemons'].sort( key= lambda x: distance(self.bot.position[0], self.bot.position[1], x['latitude'], x['longitude'])) - return self.catch_pokemon(self.bot.cell['wild_pokemons'].pop(0)) + self.catch_pokemon(self.bot.cell['wild_pokemons'].pop(0)) + + if num_catchable_pokemon > 1: + return WorkerResult.RUNNING + else: + return WorkerResult.SUCCESS def catch_pokemon(self, pokemon): worker = PokemonCatchWorker(pokemon, self.bot) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 207ed83d81..d551a68632 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -4,7 +4,7 @@ from pokemongo_bot import inventory from pokemongo_bot.base_task import BaseTask from pokemongo_bot.human_behaviour import normalized_reticle_size, sleep, spin_modifier - +from pokemongo_bot.worker_result import WorkerResult CATCH_STATUS_SUCCESS = 1 CATCH_STATUS_FAILED = 2 @@ -68,7 +68,7 @@ def work(self, response_dict=None): # validate response if not response_dict: - return False + return WorkerResult.ERROR try: responses = response_dict['responses'] response = responses[self.response_key] @@ -77,9 +77,9 @@ def work(self, response_dict=None): self.emit_event('pokemon_not_in_range', formatted='Pokemon went out of range!') elif response[self.response_status_key] == ENCOUNTER_STATUS_POKEMON_INVENTORY_FULL: self.emit_event('pokemon_inventory_full', formatted='Your Pokemon inventory is full! Could not catch!') - return False + return WorkerResult.ERROR except KeyError: - return False + return WorkerResult.ERROR # get pokemon data pokemon_data = response['wild_pokemon']['pokemon_data'] if 'wild_pokemon' in response else response['pokemon_data'] @@ -87,7 +87,7 @@ def work(self, response_dict=None): # skip ignored pokemon if not self._should_catch_pokemon(pokemon): - return False + return WorkerResult.SUCCESS # log encounter self.emit_event( diff --git a/pokemongo_bot/worker_result.py b/pokemongo_bot/worker_result.py index f38ceb9704..0e3ba10ebd 100644 --- a/pokemongo_bot/worker_result.py +++ b/pokemongo_bot/worker_result.py @@ -1,3 +1,4 @@ class WorkerResult(object): RUNNING = 'RUNNING' SUCCESS = 'SUCCESS' + ERROR = 'ERROR' From e73d302a13dac88b4e6a41553b8419e76287957c Mon Sep 17 00:00:00 2001 From: Andreas Schubert Date: Tue, 9 Aug 2016 11:19:03 +0200 Subject: [PATCH 043/143] fixed NameError: global name 'pokemon_name' is not defined (#3244) resolves ```traceback (most recent call last): File "pokecli.py", line 521, in main() File "pokecli.py", line 95, in main bot.tick() File "/usr/src/app/pokemongo_bot/__init__.py", line 451, in tick if worker.work() == WorkerResult.RUNNING: File "/usr/src/app/pokemongo_bot/cell_workers/evolve_pokemon.py", line 38, in work self._execute_pokemon_evolve(pokemon, cache) File "/usr/src/app/pokemongo_bot/cell_workers/evolve_pokemon.py", line 117, in _execute_pokemon_evolve cache[pokemon.name] = 1 NameError: global name 'pokemon_name' is not defined``` --- pokemongo_bot/cell_workers/evolve_pokemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index d045fc8fef..4cc451115b 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -114,7 +114,7 @@ def _execute_pokemon_evolve(self, pokemon, cache): return True else: # cache pokemons we can't evolve. Less server calls - cache[pokemon_name] = 1 + cache[pokemon.name] = 1 sleep(0.7) return False From 8203f360feeccd61387e5e3e52c4779c9fdc3e1c Mon Sep 17 00:00:00 2001 From: Eli White Date: Tue, 9 Aug 2016 02:19:31 -0700 Subject: [PATCH 044/143] Stop fetching gym details (#3245) --- pokemongo_bot/__init__.py | 18 ------------------ pokemongo_bot/gym_cache.py | 32 -------------------------------- 2 files changed, 50 deletions(-) delete mode 100644 pokemongo_bot/gym_cache.py diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 6f45034f1a..f0f98ba157 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -25,7 +25,6 @@ from human_behaviour import sleep from item_list import Item from metrics import Metrics -from gym_cache import GymCache from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler from pokemongo_bot.socketio_server.runner import SocketIoRunner from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl @@ -62,7 +61,6 @@ def __init__(self, config): ) self.item_list = json.load(open(os.path.join('data', 'items.json'))) self.metrics = Metrics(self) - self.gym_cache = GymCache(self) self.latest_inventory = None self.cell = None self.recent_forts = [None] * config.forts_max_circle_size @@ -496,22 +494,6 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): location = self.position[0:2] cells = self.find_close_cells(*location) - # insert detail info about gym to fort - for cell in cells: - if 'forts' in cell: - for fort in cell['forts']: - if fort.get('type') != 1: - response_gym_details = self.gym_cache.get( - gym_id=fort.get('id'), - player_latitude=lng, - player_longitude=lat, - gym_latitude=fort.get('latitude'), - gym_longitude=fort.get('longitude') - ) - fort['gym_details'] = response_gym_details.get( - 'responses', {} - ).get('GET_GYM_DETAILS', None) - user_data_cells = "data/cells-%s.json" % self.config.username with open(user_data_cells, 'w') as outfile: json.dump(cells, outfile) diff --git a/pokemongo_bot/gym_cache.py b/pokemongo_bot/gym_cache.py deleted file mode 100644 index b7bc7eb1eb..0000000000 --- a/pokemongo_bot/gym_cache.py +++ /dev/null @@ -1,32 +0,0 @@ -import time - -class GymCache(object): - def __init__(self, bot): - self.bot = bot - self.cache = {} - self.cache_length_seconds = 60 * 10 - - def get(self, gym_id, player_latitude, player_longitude, gym_latitude, gym_longitude): - if gym_id not in self.cache: - response_gym_details = self.bot.api.get_gym_details( - gym_id=gym_id, - player_latitude=player_latitude, - player_longitude=player_longitude, - gym_latitude=gym_latitude, - gym_longitude=gym_longitude - ) - - self.cache[gym_id] = response_gym_details - - gym_info = self.cache[gym_id] - gym_info['last_accessed'] = time.time() - - self._remove_stale_gyms() - return gym_info - - def _remove_stale_gyms(self): - for gym_id, gym_details in self.cache.items(): - if gym_details['last_accessed'] < time.time() - self.cache_length_seconds: - del self.cache[gym_id] - - From 4c95259c25543b483a539a928ae67e7e590778f3 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Tue, 9 Aug 2016 11:20:20 +0200 Subject: [PATCH 045/143] Checking all forts for lured pokemon (#3163) --- .../cell_workers/catch_lured_pokemon.py | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/pokemongo_bot/cell_workers/catch_lured_pokemon.py b/pokemongo_bot/cell_workers/catch_lured_pokemon.py index 10a046dce9..187c1eb40d 100644 --- a/pokemongo_bot/cell_workers/catch_lured_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_lured_pokemon.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from pokemongo_bot.cell_workers.utils import fort_details -from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.constants import Constants +from pokemongo_bot.cell_workers.utils import fort_details, distance +from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker class CatchLuredPokemon(BaseTask): @@ -11,24 +13,42 @@ class CatchLuredPokemon(BaseTask): def work(self): lured_pokemon = self.get_lured_pokemon() - if lured_pokemon: - self.catch_pokemon(lured_pokemon) + if len(lured_pokemon) > 0: + self.catch_pokemon(lured_pokemon[0]) + + if len(lured_pokemon) > 1: + return WorkerResult.RUNNING + + return WorkerResult.SUCCESS def get_lured_pokemon(self): + forts_in_range = [] + pokemon_to_catch = [] forts = self.bot.get_forts(order_by_distance=True) if len(forts) == 0: return False - fort = forts[0] - details = fort_details(self.bot, fort_id=fort['id'], - latitude=fort['latitude'], - longitude=fort['longitude']) - fort_name = details.get('name', 'Unknown') + for fort in forts: + distance_to_fort = distance( + self.bot.position[0], + self.bot.position[1], + fort['latitude'], + fort['longitude'] + ) + + encounter_id = fort.get('lure_info', {}).get('encounter_id', None) + if distance_to_fort < Constants.MAX_DISTANCE_FORT_IS_REACHABLE and encounter_id: + forts_in_range.append(fort) - encounter_id = fort.get('lure_info', {}).get('encounter_id', None) - if encounter_id: + for fort in forts_in_range: + details = fort_details(self.bot, fort_id=fort['id'], + latitude=fort['latitude'], + longitude=fort['longitude']) + fort_name = details.get('name', 'Unknown') + encounter_id = fort['lure_info']['encounter_id'] + result = { 'encounter_id': encounter_id, 'fort_id': fort['id'], @@ -36,15 +56,14 @@ def get_lured_pokemon(self): 'latitude': fort['latitude'], 'longitude': fort['longitude'] } + pokemon_to_catch.append(result) self.emit_event( 'lured_pokemon_found', formatted='Lured pokemon at fort {fort_name} ({fort_id})', data=result ) - return result - - return False + return pokemon_to_catch def catch_pokemon(self, pokemon): worker = PokemonCatchWorker(pokemon, self.bot) From d42082349736c83616d5c0d42acf43d8abb0b745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Phan=20H=E1=BA=A3i=20Phong?= Date: Tue, 9 Aug 2016 16:48:29 +0700 Subject: [PATCH 046/143] Fix flooding of keep_best_release (#3223) * Fix flooding of keep_best_release * Fix flooding of keep_best_release --- .../cell_workers/transfer_pokemon.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 5c1d30fae7..ebc197ef24 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -43,17 +43,6 @@ def work(self): all_pokemons.remove(pokemon) best_pokemons.append(pokemon) - if best_pokemons and all_pokemons: - self.emit_event( - 'keep_best_release', - formatted="Keeping best {amount} {pokemon}, based on {criteria}", - data={ - 'amount': len(best_pokemons), - 'pokemon': pokemon_name, - 'criteria': order_criteria - } - ) - transfer_pokemons = [pokemon for pokemon in all_pokemons if self.should_release_pokemon(pokemon_name, pokemon['cp'], @@ -61,6 +50,16 @@ def work(self): True)] if transfer_pokemons: + if best_pokemons: + self.emit_event( + 'keep_best_release', + formatted="Keeping best {amount} {pokemon}, based on {criteria}", + data={ + 'amount': len(best_pokemons), + 'pokemon': pokemon_name, + 'criteria': order_criteria + } + ) for pokemon in transfer_pokemons: self.release_pokemon(pokemon_name, pokemon['cp'], pokemon['iv'], pokemon['pokemon_data']['id']) else: From 0f9351e882a0b2f48c5d411e8ed9d990283e009c Mon Sep 17 00:00:00 2001 From: Nikhil Pandey Date: Tue, 9 Aug 2016 16:19:09 +0545 Subject: [PATCH 047/143] [Feature] Recycle Threshold (#2465) * Add Threshold Option * Add Threshold Option to Example Configs * Add Name to Contributors * Change config name and message * Remove logger * Add option to run when storage less than something * Change Message * Fix * Error fixes, message improvement * Config Changes and Remove Option --- CONTRIBUTORS.md | 1 + configs/config.json.cluster.example | 1 + configs/config.json.example | 1 + configs/config.json.map.example | 1 + configs/config.json.path.example | 1 + configs/config.json.pokemon.example | 1 + pokemongo_bot/__init__.py | 4 ++++ pokemongo_bot/cell_workers/recycle_items.py | 23 +++++++++++++++++++-- 8 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 89011f7c38..fd219fe470 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -56,3 +56,4 @@ * mjmadsen * nikofil * bigkraig + * nikhil-pandey diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index b32eb4f668..e4597519f6 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/configs/config.json.example b/configs/config.json.example index ec46c15eb9..d2e8d4f064 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/configs/config.json.map.example b/configs/config.json.map.example index 43196196f5..a729c57274 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -33,6 +33,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 94a9fdba07..dec7457246 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 1d428a6ae7..e7ba38dc37 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index f0f98ba157..dd4de22551 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -349,6 +349,10 @@ def _register_events(self): 'amount', 'item', 'maximum' ) ) + self.event_manager.register_event( + 'item_discard_skipped', + parameters=('space',) + ) self.event_manager.register_event( 'item_discard_fail', parameters=('item',) diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 2c969913b0..673b373fba 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -3,10 +3,12 @@ from pokemongo_bot.base_task import BaseTask from pokemongo_bot.tree_config_builder import ConfigException + class RecycleItems(BaseTask): SUPPORTED_TASK_API_VERSION = 1 def initialize(self): + self.min_empty_space = self.config.get('min_empty_space', None) self.item_filter = self.config.get('item_filter', {}) self._validate_item_filter() @@ -15,9 +17,26 @@ def _validate_item_filter(self): for config_item_name, bag_count in self.item_filter.iteritems(): if config_item_name not in item_list.viewvalues(): if config_item_name not in item_list: - raise ConfigException("item {} does not exist, spelling mistake? (check for valid item names in data/items.json)".format(config_item_name)) + raise ConfigException( + "item {} does not exist, spelling mistake? (check for valid item names in data/items.json)".format( + config_item_name)) def work(self): + items_in_bag = self.bot.get_inventory_count('item') + total_bag_space = self.bot.player_data['max_item_storage'] + free_bag_space = total_bag_space - items_in_bag + + if self.min_empty_space is not None: + if free_bag_space >= self.min_empty_space: + self.emit_event( + 'item_discard_skipped', + formatted="Skipping Recycling of Items. {space} space left in bag.", + data={ + 'space': free_bag_space + } + ) + return + self.bot.latest_inventory = None item_count_dict = self.bot.item_inventory_count('all') @@ -58,7 +77,7 @@ def work(self): def send_recycle_item_request(self, item_id, count): # Example of good request response - #{'responses': {'RECYCLE_INVENTORY_ITEM': {'result': 1, 'new_count': 46}}, 'status_code': 1, 'auth_ticket': {'expire_timestamp_ms': 1469306228058L, 'start': '/HycFyfrT4t2yB2Ij+yoi+on778aymMgxY6RQgvrGAfQlNzRuIjpcnDd5dAxmfoTqDQrbz1m2dGqAIhJ+eFapg==', 'end': 'f5NOZ95a843tgzprJo4W7Q=='}, 'request_id': 8145806132888207460L} + # {'responses': {'RECYCLE_INVENTORY_ITEM': {'result': 1, 'new_count': 46}}, 'status_code': 1, 'auth_ticket': {'expire_timestamp_ms': 1469306228058L, 'start': '/HycFyfrT4t2yB2Ij+yoi+on778aymMgxY6RQgvrGAfQlNzRuIjpcnDd5dAxmfoTqDQrbz1m2dGqAIhJ+eFapg==', 'end': 'f5NOZ95a843tgzprJo4W7Q=='}, 'request_id': 8145806132888207460L} return self.bot.api.recycle_inventory_item( item_id=item_id, count=count From 49f9177ef4226a6a69499d7afc1fb24e5c72fc50 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Tue, 9 Aug 2016 05:53:28 -0500 Subject: [PATCH 048/143] Call heartbeat on step_walker even if speed is higher than distance (#2513) --- pokemongo_bot/step_walker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pokemongo_bot/step_walker.py b/pokemongo_bot/step_walker.py index 263699095d..f6c2cbfe96 100644 --- a/pokemongo_bot/step_walker.py +++ b/pokemongo_bot/step_walker.py @@ -39,6 +39,7 @@ def __init__(self, bot, speed, dest_lat, dest_lng): def step(self): if (self.dLat == 0 and self.dLng == 0) or self.dist < self.speed: self.api.set_position(self.destLat, self.destLng, 0) + self.bot.heartbeat() return True totalDLat = (self.destLat - self.initLat) From 59f55fd38f7eb0d2677c650b039b0d9277fceba2 Mon Sep 17 00:00:00 2001 From: Joao Poupino Date: Tue, 9 Aug 2016 12:30:00 +0100 Subject: [PATCH 049/143] Return an empty list if no pokemon are available. (#3259) The changes introduced in 4c95259 expose this bug. --- pokemongo_bot/cell_workers/catch_lured_pokemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/catch_lured_pokemon.py b/pokemongo_bot/cell_workers/catch_lured_pokemon.py index 187c1eb40d..afddeb53d5 100644 --- a/pokemongo_bot/cell_workers/catch_lured_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_lured_pokemon.py @@ -27,7 +27,7 @@ def get_lured_pokemon(self): forts = self.bot.get_forts(order_by_distance=True) if len(forts) == 0: - return False + return [] for fort in forts: distance_to_fort = distance( From 79266a0ccfedcf04b690d23d5373c95024f89681 Mon Sep 17 00:00:00 2001 From: mmns Date: Tue, 9 Aug 2016 15:21:27 +0200 Subject: [PATCH 050/143] Allow UpdateTitleStats to emit events instead of rewriting the console (#3264) --- pokemongo_bot/cell_workers/update_title_stats.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index acbfaa7fe4..0a6e3c592d 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -70,6 +70,8 @@ def __init__(self, bot, config): self.min_interval = self.DEFAULT_MIN_INTERVAL self.displayed_stats = self.DEFAULT_DISPLAYED_STATS + self.bot.event_manager.register_event('update_title', parameters=('title')) + self._process_config() def initialize(self): @@ -109,6 +111,15 @@ def _update_title(self, title, platform): :rtype: None :raise: RuntimeError: When the given platform isn't supported. """ + + self.emit_event( + 'update_title', + formatted="{title}", + data={ + 'title': title + } + ) + if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) stdout.flush() From b3d8d8650120798961681e7e25251c4839121b46 Mon Sep 17 00:00:00 2001 From: Eli White Date: Tue, 9 Aug 2016 06:22:07 -0700 Subject: [PATCH 051/143] Updating our issue and PR templates to be more helpful (#3262) --- .github/ISSUE_TEMPLATE.md | 12 ++++++++++-- .github/PULL_REQUEST_TEMPLATE.md | 18 +++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index a1d7168e75..e5a3f1f317 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,7 @@ -Please check configuration at http://jsonlint.com/ before posting an issue. +## Stop! Before you create this issue (you can delete this section when opening the issue): +1. Have you validated that your config.json is valid JSON? Use http://jsonlint.com/ to check. +2. Have you searched to see if there are other issues for the same issue? If so, comment on that issue instead. +3. Are you running `master`? We work on the `dev` branch and then add that functionality to `master`. Your issue may be fixed on `dev` and there is no need for this issue, just wait and it will eventually be merged to `master`. ### Expected Behavior @@ -6,10 +9,15 @@ Please check configuration at http://jsonlint.com/ before posting an issue. ### Actual Behavior +### Your config.json (remove your credentials and any other private info) +``` +your config here +``` + ### Steps to Reproduce ### Other Information -OS: +OS: Git Commit: (run 'git log -n 1 --pretty=format:"%H"' and paste it here) Python Version: (run 'python -V' and paste it here) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5178d928b5..a43df95477 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,15 @@ -Short Description: +# Please Note (you may remove this section before opening your PR): +We receive lots of PRs and it is hard to give proper review to PRs. Please make it easy on us by following these guidelines: -Fixes: -- -- -- +1. We do not accept changes to `master`. Please make sure your pull request is aimed at `dev`. +2. If you changed a bunch of files (that aren't config files) or multiple workers to implement your feature, it probably won't get proper attention. Please split it up into multiple, smaller, more focused, and iterative PRs if you can. +3. If you are adding a config value to something, make sure you update the appropriate `config.json` example files. + + +## Short Description: + +## Fixes (provide links to github issues if you can): +- +- +- From 1060afa55ee9caa1502df8a90f959d0d461cdb35 Mon Sep 17 00:00:00 2001 From: Raiven66 Date: Tue, 9 Aug 2016 15:27:08 +0200 Subject: [PATCH 052/143] Dev (#3277) * * adding enhanced sniping capabilities for move_to_map_pokemon * Adding enhanced sniping capabilities for move_to_map_pokemon --- configs/config.json.map.example | 4 +++- pokemongo_bot/cell_workers/move_to_map_pokemon.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/configs/config.json.map.example b/configs/config.json.map.example index a729c57274..bb6878f5ac 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -61,7 +61,9 @@ "min_time": 60, "min_ball": 50, "prioritize_vips": true, - "snipe": false, + "snipe": true, + "snipe_high_prio_only": true, + "snipe_high_prio_threshold": 400, "update_map": true, "mode": "priority", "catch": { diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index c97b240196..2cfd45d14b 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -255,7 +255,11 @@ def work(self): return WorkerResult.SUCCESS if self.config['snipe']: - return self.snipe(pokemon) + if self.config['snipe_high_prio_only']: + if self.config['snipe_high_prio_threshold'] < pokemon['priority'] or pokemon['is_vip']: + self.snipe(pokemon) + else: + return self.snipe(pokemon) step_walker = self._move_to(pokemon) if not step_walker.step(): From f4b4c282dcaa1a26676fbae5951f1278dd3c86ce Mon Sep 17 00:00:00 2001 From: Erik Weber Date: Tue, 9 Aug 2016 15:28:27 +0200 Subject: [PATCH 053/143] Update pgoapi to a newer version (#3241) This should hopefully fix issues like #3181, #3098, #2874 and potentially more. Needs testing/verification. I am running now, but it does take about an hour to trigger. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f6a22a0233..76b1d15a7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy==1.11.0 networkx==1.11 --e git+https://github.com/keyphact/pgoapi.git@249d3be7fbbdabc7f9adea17cbc899d6549e47a2#egg=pgoapi +-e git+https://github.com/keyphact/pgoapi.git@a2755eb42dfe49e359798d2f4defefc97fb8163d#egg=pgoapi geopy==1.11.0 protobuf==3.0.0b4 requests==2.10.0 From 3e1dc1b76f3ae5e134a17a3a974f865494d558d3 Mon Sep 17 00:00:00 2001 From: Eevee Date: Tue, 9 Aug 2016 22:29:02 +0900 Subject: [PATCH 054/143] Fix unexpected egg incubation retry (#3276) incubator['used'] flag is set but not used in IncubateEggs._apply_incubators --- pokemongo_bot/cell_workers/incubate_eggs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pokemongo_bot/cell_workers/incubate_eggs.py b/pokemongo_bot/cell_workers/incubate_eggs.py index 5761090ea5..805129435b 100644 --- a/pokemongo_bot/cell_workers/incubate_eggs.py +++ b/pokemongo_bot/cell_workers/incubate_eggs.py @@ -49,6 +49,8 @@ def work(self): def _apply_incubators(self): for incubator in self.ready_incubators: + if incubator.get('used', False): + continue for egg in self.eggs: if egg["used"] or egg["km"] == -1: continue From 0f2bddd26675c5232b2e9f4e83dc6525c41a842d Mon Sep 17 00:00:00 2001 From: Jordan Christensen Date: Tue, 9 Aug 2016 09:31:21 -0400 Subject: [PATCH 055/143] has_next_evolution is a function not a property (#3284) --- CONTRIBUTORS.md | 1 + pokemongo_bot/inventory.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index fd219fe470..6e0e44f5fd 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -57,3 +57,4 @@ * nikofil * bigkraig * nikhil-pandey + * thebigjc diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index 1cd8a893b2..c74a85296f 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -165,7 +165,7 @@ def __init__(self, data): self.iv = self._compute_iv() def can_evolve_now(self): - return self.has_next_evolution and self.candy_quantity > self.evolution_cost + return self.has_next_evolution() and self.candy_quantity > self.evolution_cost def has_next_evolution(self): return 'Next Evolution Requirements' in self._static_data From 2ded2ee9c70bc4d6f7b2b3128535a81850cef2b2 Mon Sep 17 00:00:00 2001 From: Simon Shi Date: Wed, 10 Aug 2016 00:28:33 +0800 Subject: [PATCH 056/143] Powerful setup.sh (#3263) * Rewrite run.sh Very powerful run.sh with lots of function. 1.install(make .so) 2.update 3.config generator 4.config backup 5.run loop make it never down It should run like run.sh *.json or other opinion. See -help. * Update run.sh * Update run.sh OK problem solved * Delete setup.py * Rename run.sh to setup.sh * Create run.sh * Update setup.sh * Update install.sh * Update setup.sh * Update run.sh * Update setup.sh Some small fix. --- install.sh | 79 ++++++++++++++++++++++++------- run.sh | 33 ++++++------- setup.py | 13 ------ setup.sh | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 50 deletions(-) mode change 100755 => 100644 run.sh delete mode 100644 setup.py create mode 100755 setup.sh diff --git a/install.sh b/install.sh index c11992bb0c..32cc1e1124 100755 --- a/install.sh +++ b/install.sh @@ -1,19 +1,62 @@ #!/usr/bin/env bash - -# Setup Python virtualenv -echo "Setting up Python virtualenv..." -eval "virtualenv ." -eval "source bin/activate" -echo "Python virtualenv setup successfully." - -# Install pip requirements -echo "Installing pip requirements..." -eval "pip install -r requirements.txt" -echo "Installed pip requirements." -echo "Installing and updating git submodules..." - -# Install git submodules -eval "cd ./web && git submodule init && cd .." -eval "git submodule update" -echo "Done." -echo "Please create and setup configs/config.json. Then, run 'python pokecli.py --config ./configs/config.json' or './run.sh' on Mac/Linux" \ No newline at end of file +pokebotpath=$(pwd) +cd $pokebotpath +if [ -f /etc/debian_version ] +then +echo "You are on Debian/Ubuntu" +sudo apt-get update +sudo apt-get -y install python python-pip python-dev build-essential git python-protobuf virtualenv +elif [ -f /etc/redhat-release ] +then +echo "You are on CentOS/RedHat" +sudo yum -y install epel-release +sudo yum -y install python-pip +elif [ "$(uname -s)" == "Darwin" ] +then +echo "You are on Mac os" +sudo brew update +sudo brew install --devel protobuf +else +echo "Nothing happend." +fi +pip install virtualenv +cd $pokebotpath +git pull +git submodule init +git submodule foreach git pull origin master +virtualenv . +source bin/activate +pip install -r requirements.txt +echo "Start to make encrypt.so" +wget http://pgoapi.com/pgoencrypt.tar.gz +tar -xf pgoencrypt.tar.gz +cd pgoencrypt/src/ +make +mv libencrypt.so $pokebotpath/encrypt.so +cd ../.. +rm -rf pgoencrypt.tar.gz +rm -rf pgoencrypt +echo "Install complete." +cd $pokebotpath +read -p "1.google 2.ptc +" auth +read -p "Input username +" username +read -p "Input password +" -s password +read -p " +Input location +" location +read -p "Input gmapkey +" gmapkey +cp configs/config.json.example configs/config.json +if [ "$auth" = "2" ] +then +sed -i "s/google/ptc/g" configs/config.json +fi +sed -i "s/YOUR_USERNAME/$username/g" configs/config.json +sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json +sed -i "s/SOME_LOCATION/$location/g" configs/config.json +sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json +echo "Edit configs/config.json to modify any other config. Use run.sh to run." +exit 0 diff --git a/run.sh b/run.sh old mode 100755 new mode 100644 index d7ef7079b7..0812dfac8c --- a/run.sh +++ b/run.sh @@ -1,26 +1,21 @@ #!/usr/bin/env bash - -# Starts PokemonGo-Bot -config="" - +pokebotpath=$(pwd) +filename="" if [ ! -z $1 ]; then - config=$1 +filename=$1 else - config="./configs/config.json" - if [ ! -f ${config} ]; then - echo -e "There's no ./configs/config.json file" - echo -e "Please create one or use another config file" - echo -e "./run.sh [path/to/config/file]" - exit 1 - fi +filename="./configs/config.json" +if [ ! -f "$filename" ] +then +echo "There's no "$filename" file. use setup.sh -config to creat one." +fi fi -while [ true ] +while true do -echo "###############################################" -echo "##Exit two times with [Ctl+C] to end the loop##" -echo "###############################################" -sleep 1 -python pokecli.py --config ${config} -sleep "10" +cd $pokebotpath +python pokecli.py -cf $filename +read -p "Press any button or wait 20 seconds." -r -s -n1 -t 20 +echo `date`"Pokebot"$*" Stopped." done +exit 0 diff --git a/setup.py b/setup.py deleted file mode 100644 index 6ccf893c0d..0000000000 --- a/setup.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python - -from pip.req import parse_requirements - -install_reqs = parse_requirements("requirements.txt", session=False) - -reqs = [str(ir.req) for ir in install_reqs] - -setup(name='pgoapi', - version='1.0', - url='https://github.com/tejado/pgoapi', - packages=['pgoapi'], - install_requires=reqs) diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000000..bcff0feefb --- /dev/null +++ b/setup.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +pokebotpath=$(pwd) +backuppath=$(pwd)"/backup" + +function Pokebotupdate () { +cd $pokebotpath +git pull +git submodule init +git submodule foreach git pull origin master +virtualenv . +source bin/activate +pip install -r requirements.txt +} + +function Pokebotencrypt () { +echo "Start to make encrypt.so" +wget http://pgoapi.com/pgoencrypt.tar.gz +tar -xf pgoencrypt.tar.gz +cd pgoencrypt/src/ +make +mv libencrypt.so $pokebotpath/encrypt.so +cd ../.. +rm -rf pgoencrypt.tar.gz +rm -rf pgoencrypt +} + +function Pokebotconfig () { +cd $pokebotpath +read -p "1.google 2.ptc +" auth +read -p "Input username +" username +read -p "Input password +" -s password +read -p " +Input location +" location +read -p "Input gmapkey +" gmapkey +cp configs/config.json.example configs/config.json +if [ "$auth" = "2" ] +then +sed -i "s/google/ptc/g" configs/config.json +fi +sed -i "s/YOUR_USERNAME/$username/g" configs/config.json +sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json +sed -i "s/SOME_LOCATION/$location/g" configs/config.json +sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json +echo "Edit configs/config.json to modify any other config." +} + +function Pokebotinstall () { +cd $pokebotpath +if [ -f /etc/debian_version ] +then +echo "You are on Debian/Ubuntu" +sudo apt-get update +sudo apt-get -y install python python-pip python-dev build-essential git python-protobuf virtualenv +elif [ -f /etc/redhat-release ] +then +echo "You are on CentOS/RedHat" +sudo yum -y install epel-release +sudo yum -y install python-pip +elif [ "$(uname -s)" == "Darwin" ] +then +echo "You are on Mac os" +sudo brew update +sudo brew install --devel protobuf +else +echo "Nothing happend." +fi +sudo pip install virtualenv +Pokebotupdate +Pokebotencrypt +echo "Install complete." +Pokebotconfig +} + +function Pokebotreset () { +cd $pokebotpath +git fetch --all +git reset --hard origin/dev +Pokebotupdate +} + +function Pokebothelp () { +echo "usage:" +echo " -i,--install. Install PokemonGo-Bot." +echo " -b,--backup. Backup config files." +echo " -c,--config. Easy config generator." +echo " -e,--encrypt. Make encrypt.so." +echo " -r,--reset. Force sync dev branch." +echo " -u,--update. Command git pull to update." +} + +case $* in +--install|-i) +Pokebotinstall +;; +--encrypt|-e) +Pokebotencrypt +;; +--reset|-r) +Pokebotreset +;; +--update|-u) +Pokebotupdate +;; +--backup|-b) +mkdir $backuppath +cp -f $pokebotpath/configs/config*.json $backuppath/ +cp -f $pokebotpath/web/config/userdata.js $backuppath/ +echo "Backup complete" +;; +--config|-c) +Pokebotconfig +;; +--help|-h) +Pokebothelp +;; +*.json) +filename=$* +cd $pokebotpath +if [ ! -f ./configs/"$filename" ] +then +echo "There's no ./configs/"$filename" file. It's better to use run.sh not this one." +else +Pokebotrun +fi +;; +*) +Pokebothelp +;; +esac +exit 0 From e03f8341bb536dea3bc9753d705ea21e20a31418 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Tue, 9 Aug 2016 09:32:05 -0700 Subject: [PATCH 057/143] Added +x to run.sh --- run.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 run.sh diff --git a/run.sh b/run.sh old mode 100644 new mode 100755 From c8aaf4b82ca2733897a1a32dac203e23ca605a8d Mon Sep 17 00:00:00 2001 From: Jaap Moolenaar Date: Tue, 9 Aug 2016 18:38:17 +0200 Subject: [PATCH 058/143] Added a configuration option "path_startmode" (conflict merge #2489) (#3270) * Upstream update and merge, with path_startmode configuration * Removed logger and fixed base task path * As per request, path_startmode is now path_start_mode * Removed all logging --- CONTRIBUTORS.md | 1 + configs/config.json.path.example | 1 + pokemongo_bot/cell_workers/follow_path.py | 32 ++++++++++++++++++++++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 6e0e44f5fd..57b289ab90 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -58,3 +58,4 @@ * bigkraig * nikhil-pandey * thebigjc + * JaapMoolenaar diff --git a/configs/config.json.path.example b/configs/config.json.path.example index dec7457246..6f7b04c305 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -60,6 +60,7 @@ "type": "FollowPath", "config": { "path_mode": "loop", + "path_start_mode": "first", "path_file": "configs/path.example.json" } } diff --git a/pokemongo_bot/cell_workers/follow_path.py b/pokemongo_bot/cell_workers/follow_path.py index 6e183ed1d7..1532695bd8 100644 --- a/pokemongo_bot/cell_workers/follow_path.py +++ b/pokemongo_bot/cell_workers/follow_path.py @@ -14,13 +14,19 @@ class FollowPath(BaseTask): SUPPORTED_TASK_API_VERSION = 1 def initialize(self): - self.ptr = 0 self._process_config() self.points = self.load_path() + if self.path_start_mode == 'closest': + self.ptr = self.find_closest_point_idx(self.points) + + else: + self.ptr = 0 + def _process_config(self): self.path_file = self.config.get("path_file", None) self.path_mode = self.config.get("path_mode", "linear") + self.path_start_mode = self.config.get("path_start_mode", "first") def load_path(self): if self.path_file is None: @@ -67,6 +73,30 @@ def load_gpx(self): return points + def find_closest_point_idx(self, points): + + return_idx = 0 + min_distance = float("inf"); + for index in range(len(points)): + point = points[index] + botlat = self.bot.api._position_lat + botlng = self.bot.api._position_lng + lat = float(point['lat']) + lng = float(point['lng']) + + dist = distance( + botlat, + botlng, + lat, + lng + ) + + if dist < min_distance: + min_distance = dist + return_idx = index + + return return_idx + def work(self): point = self.points[self.ptr] lat = float(point['lat']) From 80a307ebd9be44948e3b39c7d3c6457c538946c9 Mon Sep 17 00:00:00 2001 From: Eli White Date: Tue, 9 Aug 2016 10:49:09 -0700 Subject: [PATCH 059/143] Adding documentation for how to use and write plugins (#3254) * Adding documentation for how to use and write plugins * Adding a link to the plugins docs in the Readme --- README.md | 3 +-- docs/plugins.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 docs/plugins.md diff --git a/README.md b/README.md index fd99669e74..592ac5a919 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,6 @@ You can count on the community in #help channel. - [Features](#features) - [Wiki](#wiki) - [Credits](#credits) -- [Donation](#donation) - ## Features - [x] GPS Location configuration @@ -56,6 +54,7 @@ All information on [Getting Started](https://github.com/PokemonGoF/PokemonGo-Bot - [Mac] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-mac) - [Windows] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-windows) - [Develop PokemonGo-Bot](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Develop-PokemonGo-Bot) +- [Plugins](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev/wiki/plugins.md) - [Configuration-files](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files) - [Front end web module - Google Maps API] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page)) - [Docker Usage](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ#how-to-run-with-docker) diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 0000000000..d2aaff50c2 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,66 @@ +# PokemonGo-Bot Plugins +Plugins are collections of tasks distributed outside of the main repo. Using plugins lets you use community built tasks that haven't been accepted into the core bot. Some tasks are better suited to not live in the main bot. An example might be a task that reports seen pokemon to a central server. + +## Using Plugins +Plugins are used by adding some new information to your `config.json`. + +In your `config.json`, you can add a new array: + +``` + ... + "plugins": [ + ], + ... +``` + +In this array, you can put a Github URL that contains the revision you want to use. For example: + +``` + ... + "plugins": [ + "TheSavior/test-pgo-plugin#2d54eddde33061be9b329efae0cfb9bd58842655" + ], + ... +``` + +Once that is there, you can add to your `tasks` array the task you want to use from the plugin. Plugins can expose many tasks, check the plugin's documentation for what tasks can be used. + +``` + ... + "tasks": [ + { + "type": "test-pgo-plugin.PrintText" + } + ] + .. +``` + +Then start the bot. It will download the specified plugins and use them when requested in your `tasks` list. + +## Developing Plugins +The plugins array can be given a full path to a folder containing a plugin as well as the github url format. When developing a plugin, use a directory outside the root of the bot and add it to your plugins array. Unlike the github url format, it won't be copied to the bot when it is started up, it will be loaded directly from the specified directory. + +Plugins have access to any of the things that the tasks in the official repo have access to. + +### Example +I recommend looking at this plugin for an example of how to write a plugin: https://github.com/TheSavior/test-pgo-plugin + +### API Versioning +We may at some point need to make a backwards incompatible change to the plugin BaseTask. We will avoid this as much as possible, but in the event that occurs, this is how incompatibilities are detected: + +The `BaseTask` class specifies: + +``` +class BaseTask(object): + TASK_API_VERSION = 1 +``` + +When we need to make a backwards incompatible change, we will increment that number. Plugins have the following: + +``` +class PrintText(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 +``` + +If a user tries to use a plugin that has a `SUPPORTED_TASK_API_VERSION` that does not match the current bot's `TASK_API_VERSION`, an exception will be raised. + From 932fd2bb93bf0a6f8bee68f49917b0ef341ee586 Mon Sep 17 00:00:00 2001 From: Eli White Date: Tue, 9 Aug 2016 10:50:45 -0700 Subject: [PATCH 060/143] Updating link to the plugin docs in the readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 592ac5a919..d372870ca6 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ All information on [Getting Started](https://github.com/PokemonGoF/PokemonGo-Bot - [Mac] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-mac) - [Windows] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-windows) - [Develop PokemonGo-Bot](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Develop-PokemonGo-Bot) -- [Plugins](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev/wiki/plugins.md) +- [Plugins](https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/plugins.md) - [Configuration-files](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files) - [Front end web module - Google Maps API] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page)) - [Docker Usage](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ#how-to-run-with-docker) From 63f777bac0c8cdc75d07f9ef491ca8b900c0148a Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Tue, 9 Aug 2016 11:22:48 -0700 Subject: [PATCH 061/143] Dev merge to master, PR (#3334) * Adding plugin support (#2679) * Adding plugin support * Adding an empty __init__.py * Moving the base task to the project root (#2702) * Moving the base task to the project root * Moving the base class more * Changing the import again * Adding a heartbeat to the analytics (#2709) * Adding a heartbeat to the analytics * Heartbeat every 30 seconds, not every 5 * Don't double track clients * Fix 'local variable 'bot' referenced before assignment' * Providing an error if tasks don't work for the given api (#2732) * Fix for utf8 encoding when catching lured pokemon (#2720) * Fixing lure pokestop encoding * fixing lure encoding * Fix For catchable not being displayed on the web (#2719) * Fix For catchable not being displayed on the web * Update catch_visible_pokemon.py * Added encrypt.so compilation process to Dockerfile (#2695) * OS Detection for encrypt lib (#2768) Fix 32bit check, darwin and linux use the same file Make it a function Check if file exists, if not show error Define file_name first Fix return Check if file exists, if not show error Print info about paths Fix for 32/64bit detection * Fix Typo in unexpected_response_retry (#2531) fixes #2525 #2523 * Revert "changing license from MIT to GPLv3" This reverts commit 69fb64f2bf7c12e28c2bb6d2b636c6af55822448. * When the google analytics domain is blocked the bot crashed. (#2764) With a simple try / except this can be solved. Fix dirty catch all * Fixes #2698 - Prevents "Possibly searching too often" error after re-login. (#2771) * Fixes #2698 - Added api.activate_signature call to prevent issue after re-login. - Also replaced deprecated log call with event_manager emit to prevent exception being thrown. * Modified to use OS detected library path as per PR #2768 * Support loading plugins from .zip files (#2766) * Keep track of how many pokemon released (#2884) * Setting Library path to work with encrypt.so (#2899) Setting LD_LIBRARY_PATH on Dockerfile * :sparkles: Added login and username to available stats (#2494) Added a player_data property in PokemonGoBot to access player data from outside Added unit tests for login and username stats Added tests for call args when updating the window title Added a platform-specific test for window title updating on win32 platform * [dev] small fixes (#2912) * Fixed emit_event typo * Update CONTRIBUTORS.md * Changed initialization location for "bot" We use bot in main exception on 128 * Update pokecli.py * Rename load_path to load_plugin (#2947) * Adding some logic for pulling plugins from github (#2967) * flush after title update (#2977) * correctly re-raise exception to keep backtrace (#2944) * Update MoveToMapPokemon to use events instead of logger. (#2913) * Config/encrypt.so (#2964) * Add config option for libencrypt.so * Correctly set the config value and check for the file in said dir * Fixed mispelling for "formatted" variable (#2984) * Loading plugins from Github (#2992) * Checking github plugin file existence * Loading plugins from github * Fixed #3000 (#3003) Fixed syntax error on "move_to_map_pokemon.py" that makes the client crash when using this feature. * Added MaxPotion inventory count to summary. (#3015) Short Description: The Max Potion count was missing from the inventory summary. Was #2456 * Added cleanup of download and files for encrypt.so after they are no longer needed (#3011) * Fix bot not returning back after telepoting (#3014) * Fix typo: last_long -> last_lon * Whitespace cleanup * Fix bug introduced by #3037: bot not returning back * Fix Dockerfile installation (#3057) * Fix for #3045 (#3055) * Added request to check configuration (#3089) * Fixed Dockerfile - missing \ on command lines (#3096) * Fixed mispelling for "formatted" variable * Docker commands missing trailing \ * Fix for FileIO slowing bot performance.This puts the map writing into a thread and makes sure it only executes once. (#3100) * Change word usage: "fled" to "escaped" (#3118) "fled" is confusing to lot of people and is easily confused with pokemon vanishing. "escaped" is a better term. * Update the example config file (#3120) * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Rename path.example.json to path.json.example * typo: logrmation -> information (#2601) Fix a typo. I assume that it was "information" initially, but became "logrmation" when someone used replace all functionality to replace all infos with logs. But I might be totally wrong at this point, idk. Just didn't like the word and wanted to fix that typo. * Change fled to escaped (#3129) Fix an issue after PR #3118 * When JSON parsing fails, give a rough indication of why (#3137) * When JSON parsing fails, give a rough indication of why * Use the official package instead of SHA1 commit * Handle Github Download Zip Format (#3108) * Checking github plugin file existence * Loading plugins from github * Starting install code for github plugins * Updating GithubPlugin to support extracting folders * Handling github zip formats by extracting to the correct location * Refactor catch worker (#2527) * refactor catch worker * fix * few renames * add to contributors * fix * add missing behavior * fix encounter events * don't make events about ignored pokemon * Added Run-Loop (#3143) * Add files via upload modified run script wich let you run the boot in a loop(if it crashes it restarts) * Integreated Loop into run.sh modified run.sh to loop the script so that even if it crashes it automaticly restarts. * fixing loop in spin fort task (#3165) * Some love for the vim users (#3154) * Updated README with link to desktop version (#3208) * Fix for #3190 (#3197) * MoveToMap: Add minimum balls to run (#3166) * added config to ignore item count for Spin and MoveToFort (#3160) * [Inventory Management] Add a central class for caching/parsing inventory & static data (#2528) * new class to centralize inventory management * use new inventory class in evolve_pokemon * use new inventory to display # candy after catch * Keeping a cache of gym information (#3236) * New Option: "dont_nickname_favorite" (#2496) * New Option: "dont_nickname_favorite" This change (line 19) adds the option, that the user can choose, whether their favorite pokemons should also get a new nickname or not. If a user want this, then he or she has to add the line ("dont_nickname_favorite" = true) after ("nickname_template": " ... ",). * Update nickname_pokemon.py * Update * Put change to line 30 This reduce the reduce the runtime, because favorite pokemon won't be added to the list. * Restart the loop when catching pokemon and there are more to catch (#3242) * fixed NameError: global name 'pokemon_name' is not defined (#3244) resolves ```traceback (most recent call last): File "pokecli.py", line 521, in main() File "pokecli.py", line 95, in main bot.tick() File "/usr/src/app/pokemongo_bot/__init__.py", line 451, in tick if worker.work() == WorkerResult.RUNNING: File "/usr/src/app/pokemongo_bot/cell_workers/evolve_pokemon.py", line 38, in work self._execute_pokemon_evolve(pokemon, cache) File "/usr/src/app/pokemongo_bot/cell_workers/evolve_pokemon.py", line 117, in _execute_pokemon_evolve cache[pokemon.name] = 1 NameError: global name 'pokemon_name' is not defined``` * Stop fetching gym details (#3245) * Checking all forts for lured pokemon (#3163) * Fix flooding of keep_best_release (#3223) * Fix flooding of keep_best_release * Fix flooding of keep_best_release * [Feature] Recycle Threshold (#2465) * Add Threshold Option * Add Threshold Option to Example Configs * Add Name to Contributors * Change config name and message * Remove logger * Add option to run when storage less than something * Change Message * Fix * Error fixes, message improvement * Config Changes and Remove Option * Call heartbeat on step_walker even if speed is higher than distance (#2513) * Return an empty list if no pokemon are available. (#3259) The changes introduced in 4c95259 expose this bug. * Allow UpdateTitleStats to emit events instead of rewriting the console (#3264) * Updating our issue and PR templates to be more helpful (#3262) * Dev (#3277) * * adding enhanced sniping capabilities for move_to_map_pokemon * Adding enhanced sniping capabilities for move_to_map_pokemon * Update pgoapi to a newer version (#3241) This should hopefully fix issues like #3181, #3098, #2874 and potentially more. Needs testing/verification. I am running now, but it does take about an hour to trigger. * Fix unexpected egg incubation retry (#3276) incubator['used'] flag is set but not used in IncubateEggs._apply_incubators * has_next_evolution is a function not a property (#3284) * Powerful setup.sh (#3263) * Rewrite run.sh Very powerful run.sh with lots of function. 1.install(make .so) 2.update 3.config generator 4.config backup 5.run loop make it never down It should run like run.sh *.json or other opinion. See -help. * Update run.sh * Update run.sh OK problem solved * Delete setup.py * Rename run.sh to setup.sh * Create run.sh * Update setup.sh * Update install.sh * Update setup.sh * Update run.sh * Update setup.sh Some small fix. * Added +x to run.sh * Added a configuration option "path_startmode" (conflict merge #2489) (#3270) * Upstream update and merge, with path_startmode configuration * Removed logger and fixed base task path * As per request, path_startmode is now path_start_mode * Removed all logging * Adding documentation for how to use and write plugins (#3254) * Adding documentation for how to use and write plugins * Adding a link to the plugins docs in the Readme * Updating link to the plugin docs in the readme --- .github/ISSUE_TEMPLATE.md | 12 +- .github/PULL_REQUEST_TEMPLATE.md | 18 +- .gitignore | 3 + CONTRIBUTORS.md | 5 + README.md | 5 +- configs/config.json.cluster.example | 1 + configs/config.json.example | 1 + configs/config.json.map.example | 6 +- configs/config.json.path.example | 2 + configs/config.json.pokemon.example | 1 + docs/plugins.md | 66 ++ install.sh | 79 +- pokemongo_bot/__init__.py | 36 +- .../cell_workers/catch_lured_pokemon.py | 49 +- .../cell_workers/catch_visible_pokemon.py | 28 +- pokemongo_bot/cell_workers/evolve_pokemon.py | 87 +- pokemongo_bot/cell_workers/follow_path.py | 32 +- pokemongo_bot/cell_workers/incubate_eggs.py | 2 + pokemongo_bot/cell_workers/move_to_fort.py | 9 +- .../cell_workers/move_to_map_pokemon.py | 9 +- .../cell_workers/nickname_pokemon.py | 2 +- .../cell_workers/pokemon_catch_worker.py | 823 ++++++++---------- pokemongo_bot/cell_workers/recycle_items.py | 23 +- pokemongo_bot/cell_workers/spin_fort.py | 13 +- .../cell_workers/transfer_pokemon.py | 21 +- .../cell_workers/update_title_stats.py | 11 + pokemongo_bot/inventory.py | 251 ++++++ pokemongo_bot/step_walker.py | 1 + pokemongo_bot/worker_result.py | 1 + requirements.txt | 2 +- run.sh | 29 +- setup.py | 13 - setup.sh | 135 +++ 33 files changed, 1139 insertions(+), 637 deletions(-) create mode 100644 docs/plugins.md create mode 100644 pokemongo_bot/inventory.py delete mode 100644 setup.py create mode 100755 setup.sh diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index a1d7168e75..e5a3f1f317 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,7 @@ -Please check configuration at http://jsonlint.com/ before posting an issue. +## Stop! Before you create this issue (you can delete this section when opening the issue): +1. Have you validated that your config.json is valid JSON? Use http://jsonlint.com/ to check. +2. Have you searched to see if there are other issues for the same issue? If so, comment on that issue instead. +3. Are you running `master`? We work on the `dev` branch and then add that functionality to `master`. Your issue may be fixed on `dev` and there is no need for this issue, just wait and it will eventually be merged to `master`. ### Expected Behavior @@ -6,10 +9,15 @@ Please check configuration at http://jsonlint.com/ before posting an issue. ### Actual Behavior +### Your config.json (remove your credentials and any other private info) +``` +your config here +``` + ### Steps to Reproduce ### Other Information -OS: +OS: Git Commit: (run 'git log -n 1 --pretty=format:"%H"' and paste it here) Python Version: (run 'python -V' and paste it here) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5178d928b5..a43df95477 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,15 @@ -Short Description: +# Please Note (you may remove this section before opening your PR): +We receive lots of PRs and it is hard to give proper review to PRs. Please make it easy on us by following these guidelines: -Fixes: -- -- -- +1. We do not accept changes to `master`. Please make sure your pull request is aimed at `dev`. +2. If you changed a bunch of files (that aren't config files) or multiple workers to implement your feature, it probably won't get proper attention. Please split it up into multiple, smaller, more focused, and iterative PRs if you can. +3. If you are adding a config value to something, make sure you update the appropriate `config.json` example files. + + +## Short Description: + +## Fixes (provide links to github issues if you can): +- +- +- diff --git a/.gitignore b/.gitignore index 70ab34b250..4721ce0253 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,6 @@ include/ # Pip check file pip-selfcheck.json + +# Some love for the vim users +.*.sw* diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 6dceaf0918..57b289ab90 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -51,6 +51,11 @@ * matheussampaio * Abraxas000 * lucasfevi + * pokepal * Moonlight-Angel * mjmadsen * nikofil + * bigkraig + * nikhil-pandey + * thebigjc + * JaapMoolenaar diff --git a/README.md b/README.md index 171ee4f355..d372870ca6 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ PokemonGo bot is a project created by the [PokemonGoF](https://github.com/PokemonGoF) team. The project is currently setup in two main branches. `dev` and `master`. +## Help Needed on [Desktop Version](https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) + ## Please submit PR to [Dev branch](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) @@ -12,8 +14,6 @@ You can count on the community in #help channel. - [Features](#features) - [Wiki](#wiki) - [Credits](#credits) -- [Donation](#donation) - ## Features - [x] GPS Location configuration @@ -54,6 +54,7 @@ All information on [Getting Started](https://github.com/PokemonGoF/PokemonGo-Bot - [Mac] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-mac) - [Windows] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-windows) - [Develop PokemonGo-Bot](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Develop-PokemonGo-Bot) +- [Plugins](https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/plugins.md) - [Configuration-files](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files) - [Front end web module - Google Maps API] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page)) - [Docker Usage](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ#how-to-run-with-docker) diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index b32eb4f668..e4597519f6 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/configs/config.json.example b/configs/config.json.example index ec46c15eb9..d2e8d4f064 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/configs/config.json.map.example b/configs/config.json.map.example index 1079c999f9..bb6878f5ac 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -33,6 +33,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, @@ -58,8 +59,11 @@ "address": "http://localhost:5000", "max_distance": 500, "min_time": 60, + "min_ball": 50, "prioritize_vips": true, - "snipe": false, + "snipe": true, + "snipe_high_prio_only": true, + "snipe_high_prio_threshold": 400, "update_map": true, "mode": "priority", "catch": { diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 94a9fdba07..6f7b04c305 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, @@ -59,6 +60,7 @@ "type": "FollowPath", "config": { "path_mode": "loop", + "path_start_mode": "first", "path_file": "configs/path.example.json" } } diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 1d428a6ae7..e7ba38dc37 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -36,6 +36,7 @@ { "type": "RecycleItems", "config": { + "min_empty_space": 15, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 0000000000..d2aaff50c2 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,66 @@ +# PokemonGo-Bot Plugins +Plugins are collections of tasks distributed outside of the main repo. Using plugins lets you use community built tasks that haven't been accepted into the core bot. Some tasks are better suited to not live in the main bot. An example might be a task that reports seen pokemon to a central server. + +## Using Plugins +Plugins are used by adding some new information to your `config.json`. + +In your `config.json`, you can add a new array: + +``` + ... + "plugins": [ + ], + ... +``` + +In this array, you can put a Github URL that contains the revision you want to use. For example: + +``` + ... + "plugins": [ + "TheSavior/test-pgo-plugin#2d54eddde33061be9b329efae0cfb9bd58842655" + ], + ... +``` + +Once that is there, you can add to your `tasks` array the task you want to use from the plugin. Plugins can expose many tasks, check the plugin's documentation for what tasks can be used. + +``` + ... + "tasks": [ + { + "type": "test-pgo-plugin.PrintText" + } + ] + .. +``` + +Then start the bot. It will download the specified plugins and use them when requested in your `tasks` list. + +## Developing Plugins +The plugins array can be given a full path to a folder containing a plugin as well as the github url format. When developing a plugin, use a directory outside the root of the bot and add it to your plugins array. Unlike the github url format, it won't be copied to the bot when it is started up, it will be loaded directly from the specified directory. + +Plugins have access to any of the things that the tasks in the official repo have access to. + +### Example +I recommend looking at this plugin for an example of how to write a plugin: https://github.com/TheSavior/test-pgo-plugin + +### API Versioning +We may at some point need to make a backwards incompatible change to the plugin BaseTask. We will avoid this as much as possible, but in the event that occurs, this is how incompatibilities are detected: + +The `BaseTask` class specifies: + +``` +class BaseTask(object): + TASK_API_VERSION = 1 +``` + +When we need to make a backwards incompatible change, we will increment that number. Plugins have the following: + +``` +class PrintText(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 +``` + +If a user tries to use a plugin that has a `SUPPORTED_TASK_API_VERSION` that does not match the current bot's `TASK_API_VERSION`, an exception will be raised. + diff --git a/install.sh b/install.sh index c11992bb0c..32cc1e1124 100755 --- a/install.sh +++ b/install.sh @@ -1,19 +1,62 @@ #!/usr/bin/env bash - -# Setup Python virtualenv -echo "Setting up Python virtualenv..." -eval "virtualenv ." -eval "source bin/activate" -echo "Python virtualenv setup successfully." - -# Install pip requirements -echo "Installing pip requirements..." -eval "pip install -r requirements.txt" -echo "Installed pip requirements." -echo "Installing and updating git submodules..." - -# Install git submodules -eval "cd ./web && git submodule init && cd .." -eval "git submodule update" -echo "Done." -echo "Please create and setup configs/config.json. Then, run 'python pokecli.py --config ./configs/config.json' or './run.sh' on Mac/Linux" \ No newline at end of file +pokebotpath=$(pwd) +cd $pokebotpath +if [ -f /etc/debian_version ] +then +echo "You are on Debian/Ubuntu" +sudo apt-get update +sudo apt-get -y install python python-pip python-dev build-essential git python-protobuf virtualenv +elif [ -f /etc/redhat-release ] +then +echo "You are on CentOS/RedHat" +sudo yum -y install epel-release +sudo yum -y install python-pip +elif [ "$(uname -s)" == "Darwin" ] +then +echo "You are on Mac os" +sudo brew update +sudo brew install --devel protobuf +else +echo "Nothing happend." +fi +pip install virtualenv +cd $pokebotpath +git pull +git submodule init +git submodule foreach git pull origin master +virtualenv . +source bin/activate +pip install -r requirements.txt +echo "Start to make encrypt.so" +wget http://pgoapi.com/pgoencrypt.tar.gz +tar -xf pgoencrypt.tar.gz +cd pgoencrypt/src/ +make +mv libencrypt.so $pokebotpath/encrypt.so +cd ../.. +rm -rf pgoencrypt.tar.gz +rm -rf pgoencrypt +echo "Install complete." +cd $pokebotpath +read -p "1.google 2.ptc +" auth +read -p "Input username +" username +read -p "Input password +" -s password +read -p " +Input location +" location +read -p "Input gmapkey +" gmapkey +cp configs/config.json.example configs/config.json +if [ "$auth" = "2" ] +then +sed -i "s/google/ptc/g" configs/config.json +fi +sed -i "s/YOUR_USERNAME/$username/g" configs/config.json +sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json +sed -i "s/SOME_LOCATION/$location/g" configs/config.json +sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json +echo "Edit configs/config.json to modify any other config. Use run.sh to run." +exit 0 diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 8e9a28ce3a..dd4de22551 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -30,8 +30,11 @@ from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl from worker_result import WorkerResult from tree_config_builder import ConfigException, MismatchTaskApiVersion, TreeConfigBuilder +from inventory import init_inventory from sys import platform as _platform import struct + + class PokemonGoBot(object): @property def position(self): @@ -232,10 +235,12 @@ def _register_events(self): 'iv_display', ) ) + self.event_manager.register_event('no_pokeballs') self.event_manager.register_event( 'pokemon_catch_rate', parameters=( 'catch_rate', + 'ball_name', 'berry_name', 'berry_count' ) @@ -244,25 +249,28 @@ def _register_events(self): 'threw_berry', parameters=( 'berry_name', + 'ball_name', 'new_catch_rate' ) ) self.event_manager.register_event( 'threw_pokeball', parameters=( - 'pokeball', + 'ball_name', 'success_percentage', 'count_left' ) ) self.event_manager.register_event( - 'pokemon_escaped', + 'pokemon_capture_failed', parameters=('pokemon',) ) self.event_manager.register_event( 'pokemon_vanished', parameters=('pokemon',) ) + self.event_manager.register_event('pokemon_not_in_range') + self.event_manager.register_event('pokemon_inventory_full') self.event_manager.register_event( 'pokemon_caught', parameters=( @@ -281,7 +289,7 @@ def _register_events(self): self.event_manager.register_event('skip_evolve') self.event_manager.register_event('threw_berry_failed', parameters=('status_code',)) self.event_manager.register_event('vip_pokemon') - + self.event_manager.register_event('gained_candy', parameters=('quantity', 'type')) # level up stuff self.event_manager.register_event( @@ -341,6 +349,10 @@ def _register_events(self): 'amount', 'item', 'maximum' ) ) + self.event_manager.register_event( + 'item_discard_skipped', + parameters=('space',) + ) self.event_manager.register_event( 'item_discard_fail', parameters=('item',) @@ -486,22 +498,6 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): location = self.position[0:2] cells = self.find_close_cells(*location) - # insert detail info about gym to fort - for cell in cells: - if 'forts' in cell: - for fort in cell['forts']: - if fort.get('type') != 1: - response_gym_details = self.api.get_gym_details( - gym_id=fort.get('id'), - player_latitude=lng, - player_longitude=lat, - gym_latitude=fort.get('latitude'), - gym_longitude=fort.get('longitude') - ) - fort['gym_details'] = response_gym_details.get( - 'responses', {} - ).get('GET_GYM_DETAILS', None) - user_data_cells = "data/cells-%s.json" % self.config.username with open(user_data_cells, 'w') as outfile: json.dump(cells, outfile) @@ -777,6 +773,8 @@ def get_inventory(self): return self.latest_inventory def update_inventory(self): + # TODO: transition to using this inventory class everywhere + init_inventory(self) response = self.get_inventory() self.inventory = list() inventory_items = response.get('responses', {}).get('GET_INVENTORY', {}).get( diff --git a/pokemongo_bot/cell_workers/catch_lured_pokemon.py b/pokemongo_bot/cell_workers/catch_lured_pokemon.py index 10a046dce9..afddeb53d5 100644 --- a/pokemongo_bot/cell_workers/catch_lured_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_lured_pokemon.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from pokemongo_bot.cell_workers.utils import fort_details -from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.constants import Constants +from pokemongo_bot.cell_workers.utils import fort_details, distance +from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker class CatchLuredPokemon(BaseTask): @@ -11,24 +13,42 @@ class CatchLuredPokemon(BaseTask): def work(self): lured_pokemon = self.get_lured_pokemon() - if lured_pokemon: - self.catch_pokemon(lured_pokemon) + if len(lured_pokemon) > 0: + self.catch_pokemon(lured_pokemon[0]) + + if len(lured_pokemon) > 1: + return WorkerResult.RUNNING + + return WorkerResult.SUCCESS def get_lured_pokemon(self): + forts_in_range = [] + pokemon_to_catch = [] forts = self.bot.get_forts(order_by_distance=True) if len(forts) == 0: - return False + return [] - fort = forts[0] - details = fort_details(self.bot, fort_id=fort['id'], - latitude=fort['latitude'], - longitude=fort['longitude']) - fort_name = details.get('name', 'Unknown') + for fort in forts: + distance_to_fort = distance( + self.bot.position[0], + self.bot.position[1], + fort['latitude'], + fort['longitude'] + ) + + encounter_id = fort.get('lure_info', {}).get('encounter_id', None) + if distance_to_fort < Constants.MAX_DISTANCE_FORT_IS_REACHABLE and encounter_id: + forts_in_range.append(fort) - encounter_id = fort.get('lure_info', {}).get('encounter_id', None) - if encounter_id: + for fort in forts_in_range: + details = fort_details(self.bot, fort_id=fort['id'], + latitude=fort['latitude'], + longitude=fort['longitude']) + fort_name = details.get('name', 'Unknown') + encounter_id = fort['lure_info']['encounter_id'] + result = { 'encounter_id': encounter_id, 'fort_id': fort['id'], @@ -36,15 +56,14 @@ def get_lured_pokemon(self): 'latitude': fort['latitude'], 'longitude': fort['longitude'] } + pokemon_to_catch.append(result) self.emit_event( 'lured_pokemon_found', formatted='Lured pokemon at fort {fort_name} ({fort_id})', data=result ) - return result - - return False + return pokemon_to_catch def catch_pokemon(self, pokemon): worker = PokemonCatchWorker(pokemon, self.bot) diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py index 1bfed225df..654c2467b3 100644 --- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -3,13 +3,24 @@ from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker from utils import distance +from pokemongo_bot.worker_result import WorkerResult class CatchVisiblePokemon(BaseTask): SUPPORTED_TASK_API_VERSION = 1 def work(self): - if 'catchable_pokemons' in self.bot.cell and len(self.bot.cell['catchable_pokemons']) > 0: + num_catchable_pokemon = 0 + if 'catchable_pokemons' in self.bot.cell: + num_catchable_pokemon = len(self.bot.cell['catchable_pokemons']) + + num_wild_pokemon = 0 + if 'wild_pokemons' in self.bot.cell: + num_wild_pokemon = len(self.bot.cell['wild_pokemons']) + + num_available_pokemon = num_catchable_pokemon + num_wild_pokemon + + if num_catchable_pokemon > 0: # Sort all by distance from current pos- eventually this should # build graph & A* it self.bot.cell['catchable_pokemons'].sort( @@ -33,15 +44,24 @@ def work(self): } ) - return self.catch_pokemon(self.bot.cell['catchable_pokemons'].pop(0)) + self.catch_pokemon(self.bot.cell['catchable_pokemons'].pop(0)) + if num_catchable_pokemon > 1: + return WorkerResult.RUNNING + else: + return WorkerResult.SUCCESS - if 'wild_pokemons' in self.bot.cell and len(self.bot.cell['wild_pokemons']) > 0: + if num_available_pokemon > 0: # Sort all by distance from current pos- eventually this should # build graph & A* it self.bot.cell['wild_pokemons'].sort( key= lambda x: distance(self.bot.position[0], self.bot.position[1], x['latitude'], x['longitude'])) - return self.catch_pokemon(self.bot.cell['wild_pokemons'].pop(0)) + self.catch_pokemon(self.bot.cell['wild_pokemons'].pop(0)) + + if num_catchable_pokemon > 1: + return WorkerResult.RUNNING + else: + return WorkerResult.SUCCESS def catch_pokemon(self, pokemon): worker = PokemonCatchWorker(pokemon, self.bot) diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index c3903a685d..4cc451115b 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -1,3 +1,4 @@ +from pokemongo_bot import inventory from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.item_list import Item from pokemongo_bot.base_task import BaseTask @@ -25,21 +26,16 @@ def work(self): if not self._should_run(): return - response_dict = self.api.get_inventory() - inventory_items = response_dict.get('responses', {}).get('GET_INVENTORY', {}).get('inventory_delta', {}).get( - 'inventory_items', {}) - - evolve_list = self._sort_and_filter(inventory_items) + evolve_list = self._sort_and_filter() if self.evolve_all[0] != 'all': # filter out non-listed pokemons - evolve_list = filter(lambda x: x["name"] in self.evolve_all, evolve_list) + evolve_list = filter(lambda x: x.name in self.evolve_all, evolve_list) cache = {} - candy_list = self._get_candy_list(inventory_items) for pokemon in evolve_list: - if self._can_evolve(pokemon, candy_list, cache): - self._execute_pokemon_evolve(pokemon, candy_list, cache) + if pokemon.can_evolve_now(): + self._execute_pokemon_evolve(pokemon, cache) def _should_run(self): if not self.evolve_all or self.evolve_all[0] == 'none': @@ -80,86 +76,45 @@ def _should_run(self): ) return False - def _get_candy_list(self, inventory_items): - candies = {} - for item in inventory_items: - candy = item.get('inventory_item_data', {}).get('candy', {}) - family_id = candy.get('family_id', 0) - amount = candy.get('candy', 0) - if family_id > 0 and amount > 0: - family = self.bot.pokemon_list[family_id - 1]['Name'] + " candies" - candies[family] = amount - - return candies - - def _sort_and_filter(self, inventory_items): + def _sort_and_filter(self): pokemons = [] logic_to_function = { - 'or': lambda pokemon: pokemon["cp"] >= self.evolve_above_cp or pokemon["iv"] >= self.evolve_above_iv, - 'and': lambda pokemon: pokemon["cp"] >= self.evolve_above_cp and pokemon["iv"] >= self.evolve_above_iv + 'or': lambda pokemon: pokemon.cp >= self.evolve_above_cp or pokemon.iv >= self.evolve_above_iv, + 'and': lambda pokemon: pokemon.cp >= self.evolve_above_cp and pokemon.iv >= self.evolve_above_iv } - for item in inventory_items: - pokemon = item.get('inventory_item_data', {}).get('pokemon_data', {}) - pokemon_num = int(pokemon.get('pokemon_id', 0)) - 1 - next_evol = self.bot.pokemon_list[pokemon_num].get('Next Evolution Requirements', {}) - pokemon = { - 'id': pokemon.get('id', 0), - 'num': pokemon_num, - 'name': self.bot.pokemon_list[pokemon_num]['Name'], - 'cp': pokemon.get('cp', 0), - 'iv': self._compute_iv(pokemon), - 'candies_family': next_evol.get('Name', ""), - 'candies_amount': next_evol.get('Amount', 0) - } - if pokemon["id"] > 0 and pokemon["candies_amount"] > 0 and (logic_to_function[self.cp_iv_logic](pokemon)): + + for pokemon in inventory.pokemons().all(): + if pokemon.id > 0 and pokemon.has_next_evolution() and (logic_to_function[self.cp_iv_logic](pokemon)): pokemons.append(pokemon) if self.first_evolve_by == "cp": - pokemons.sort(key=lambda x: (x['num'], x["cp"], x["iv"]), reverse=True) + pokemons.sort(key=lambda x: (x.pokemon_id, x.cp, x.iv), reverse=True) else: - pokemons.sort(key=lambda x: (x['num'], x["iv"], x["cp"]), reverse=True) + pokemons.sort(key=lambda x: (x.pokemon_id, x.iv, x.cp), reverse=True) return pokemons - def _can_evolve(self, pokemon, candy_list, cache): - - if pokemon["name"] in cache: - return False - - family = pokemon["candies_family"] - amount = pokemon["candies_amount"] - if family in candy_list and candy_list[family] >= amount: - return True - else: - cache[pokemon["name"]] = 1 - return False - - def _execute_pokemon_evolve(self, pokemon, candy_list, cache): - pokemon_id = pokemon["id"] - pokemon_name = pokemon["name"] - pokemon_cp = pokemon["cp"] - pokemon_iv = pokemon["iv"] - - if pokemon_name in cache: + def _execute_pokemon_evolve(self, pokemon, cache): + if pokemon.name in cache: return False - response_dict = self.api.evolve_pokemon(pokemon_id=pokemon_id) + response_dict = self.api.evolve_pokemon(pokemon_id=pokemon.id) if response_dict.get('responses', {}).get('EVOLVE_POKEMON', {}).get('result', 0) == 1: self.emit_event( 'pokemon_evolved', formatted="Successfully evolved {pokemon} with CP {cp} and IV {iv}!", data={ - 'pokemon': pokemon_name, - 'iv': pokemon_iv, - 'cp': pokemon_cp + 'pokemon': pokemon.name, + 'iv': pokemon.iv, + 'cp': pokemon.cp } ) - candy_list[pokemon["candies_family"]] -= pokemon["candies_amount"] + inventory.candies().get(pokemon.pokemon_id).consume(pokemon.evolution_cost) sleep(self.evolve_speed) return True else: # cache pokemons we can't evolve. Less server calls - cache[pokemon_name] = 1 + cache[pokemon.name] = 1 sleep(0.7) return False diff --git a/pokemongo_bot/cell_workers/follow_path.py b/pokemongo_bot/cell_workers/follow_path.py index 6e183ed1d7..1532695bd8 100644 --- a/pokemongo_bot/cell_workers/follow_path.py +++ b/pokemongo_bot/cell_workers/follow_path.py @@ -14,13 +14,19 @@ class FollowPath(BaseTask): SUPPORTED_TASK_API_VERSION = 1 def initialize(self): - self.ptr = 0 self._process_config() self.points = self.load_path() + if self.path_start_mode == 'closest': + self.ptr = self.find_closest_point_idx(self.points) + + else: + self.ptr = 0 + def _process_config(self): self.path_file = self.config.get("path_file", None) self.path_mode = self.config.get("path_mode", "linear") + self.path_start_mode = self.config.get("path_start_mode", "first") def load_path(self): if self.path_file is None: @@ -67,6 +73,30 @@ def load_gpx(self): return points + def find_closest_point_idx(self, points): + + return_idx = 0 + min_distance = float("inf"); + for index in range(len(points)): + point = points[index] + botlat = self.bot.api._position_lat + botlng = self.bot.api._position_lng + lat = float(point['lat']) + lng = float(point['lng']) + + dist = distance( + botlat, + botlng, + lat, + lng + ) + + if dist < min_distance: + min_distance = dist + return_idx = index + + return return_idx + def work(self): point = self.points[self.ptr] lat = float(point['lat']) diff --git a/pokemongo_bot/cell_workers/incubate_eggs.py b/pokemongo_bot/cell_workers/incubate_eggs.py index 5761090ea5..805129435b 100644 --- a/pokemongo_bot/cell_workers/incubate_eggs.py +++ b/pokemongo_bot/cell_workers/incubate_eggs.py @@ -49,6 +49,8 @@ def work(self): def _apply_incubators(self): for incubator in self.ready_incubators: + if incubator.get('used', False): + continue for egg in self.eggs: if egg["used"] or egg["km"] == -1: continue diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index e4b4187d20..7dcd0977b1 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -13,17 +13,18 @@ class MoveToFort(BaseTask): def initialize(self): self.lure_distance = 0 - self.lure_attraction = True #self.config.get("lure_attraction", True) - self.lure_max_distance = 2000 #self.config.get("lure_max_distance", 2000) + self.lure_attraction = self.config.get("lure_attraction", True) + self.lure_max_distance = self.config.get("lure_max_distance", 2000) + self.ignore_item_count = self.config.get("ignore_item_count", False) def should_run(self): has_space_for_loot = self.bot.has_space_for_loot() if not has_space_for_loot: self.emit_event( 'inventory_full', - formatted="Not moving to any forts as there aren't enough space. You might want to change your config to recycle more items if this message appears consistently." + formatted="Inventory is full. You might want to change your config to recycle more items if this message appears consistently." ) - return has_space_for_loot or self.bot.softban + return has_space_for_loot or self.ignore_item_count or self.bot.softban def is_attracted(self): return (self.lure_distance > 0) diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 9ab6bbb7a2..2cfd45d14b 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -82,6 +82,7 @@ def initialize(self): self.pokemon_data = self.bot.pokemon_list self.unit = self.bot.config.distance_unit self.caught = [] + self.min_ball = self.config.get('min_ball', 1) data_file = 'data/map-caught-{}.json'.format(self.bot.config.username) if os.path.isfile(data_file): @@ -250,11 +251,15 @@ def work(self): pokemon = pokemon_list[0] # if we only have ultraballs and the target is not a vip don't snipe/walk - if (pokeballs + superballs) < 1 and not pokemon['is_vip']: + if (pokeballs + superballs) < self.min_ball and not pokemon['is_vip']: return WorkerResult.SUCCESS if self.config['snipe']: - return self.snipe(pokemon) + if self.config['snipe_high_prio_only']: + if self.config['snipe_high_prio_threshold'] < pokemon['priority'] or pokemon['is_vip']: + self.snipe(pokemon) + else: + return self.snipe(pokemon) step_walker = self._move_to(pokemon) if not step_walker.step(): diff --git a/pokemongo_bot/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py index cda206ad20..e521cadfba 100644 --- a/pokemongo_bot/cell_workers/nickname_pokemon.py +++ b/pokemongo_bot/cell_workers/nickname_pokemon.py @@ -27,7 +27,7 @@ def _get_inventory_pokemon(self,inventory_dict): except KeyError: pass else: - if not pokemon.get('is_egg',False): + if not pokemon.get('is_egg',False) and not (pokemon.get('favorite', 0) == 1 and self.config.get('dont_nickname_favorite',False)): pokemon_data.append(pokemon) return pokemon_data diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index d5118b391b..d551a68632 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -1,13 +1,50 @@ # -*- coding: utf-8 -*- import time -from pokemongo_bot.human_behaviour import (normalized_reticle_size, sleep, - spin_modifier) +from pokemongo_bot import inventory from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.human_behaviour import normalized_reticle_size, sleep, spin_modifier +from pokemongo_bot.worker_result import WorkerResult + +CATCH_STATUS_SUCCESS = 1 +CATCH_STATUS_FAILED = 2 +CATCH_STATUS_VANISHED = 3 + +ENCOUNTER_STATUS_SUCCESS = 1 +ENCOUNTER_STATUS_NOT_IN_RANGE = 5 +ENCOUNTER_STATUS_POKEMON_INVENTORY_FULL = 7 + +ITEM_POKEBALL = 1 +ITEM_GREATBALL = 2 +ITEM_ULTRABALL = 3 +ITEM_RAZZBERRY = 701 + +LOGIC_TO_FUNCTION = { + 'or': lambda x, y: x or y, + 'and': lambda x, y: x and y +} + + +class Pokemon(object): + + def __init__(self, pokemon_list, pokemon_data): + self.num = int(pokemon_data['pokemon_id']) + self.name = pokemon_list[int(self.num) - 1]['Name'] + self.cp = pokemon_data['cp'] + self.attack = pokemon_data.get('individual_attack', 0) + self.defense = pokemon_data.get('individual_defense', 0) + self.stamina = pokemon_data.get('individual_stamina', 0) + + @property + def iv(self): + return round((self.attack + self.defense + self.stamina) / 45.0, 2) + + @property + def iv_display(self): + return '{}/{}/{}'.format(self.attack, self.defense, self.stamina) + class PokemonCatchWorker(BaseTask): - BAG_FULL = 'bag_full' - NO_POKEBALLS = 'no_pokeballs' def __init__(self, pokemon, bot): self.pokemon = pokemon @@ -22,444 +59,63 @@ def __init__(self, pokemon, bot): self.response_key = '' self.response_status_key = '' + ############################################################################ + # public methods + ############################################################################ + def work(self, response_dict=None): - encounter_id = self.pokemon['encounter_id'] + response_dict = response_dict or self.create_encounter_api_call() + # validate response if not response_dict: - response_dict = self.create_encounter_api_call() - - if response_dict and 'responses' in response_dict: - if self.response_key in response_dict['responses']: - if self.response_status_key in response_dict['responses'][self.response_key]: - if response_dict['responses'][self.response_key][self.response_status_key] is 1: - cp = 0 - if 'wild_pokemon' in response_dict['responses'][self.response_key] or 'pokemon_data' in \ - response_dict['responses'][self.response_key]: - if self.response_key == 'ENCOUNTER': - pokemon = response_dict['responses'][self.response_key]['wild_pokemon'] - else: - pokemon = response_dict['responses'][self.response_key] - - catch_rate = response_dict['responses'][self.response_key]['capture_probability'][ - 'capture_probability'] # 0 = pokeballs, 1 great balls, 3 ultra balls - - if 'pokemon_data' in pokemon and 'cp' in pokemon['pokemon_data']: - pokemon_data = pokemon['pokemon_data'] - cp = pokemon_data['cp'] - - individual_attack = pokemon_data.get("individual_attack", 0) - individual_stamina = pokemon_data.get("individual_stamina", 0) - individual_defense = pokemon_data.get("individual_defense", 0) - - iv_display = '{}/{}/{}'.format( - individual_attack, - individual_defense, - individual_stamina - ) - - pokemon_potential = self.pokemon_potential(pokemon_data) - pokemon_num = int(pokemon_data['pokemon_id']) - 1 - pokemon_name = self.pokemon_list[int(pokemon_num)]['Name'] - - msg = 'A wild {pokemon} appeared! [CP {cp}] [Potential {iv}] [S/A/D {iv_display}]' - self.emit_event( - 'pokemon_appeared', - formatted=msg, - data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': pokemon_potential, - 'iv_display': iv_display, - } - ) - - pokemon_data['name'] = pokemon_name - # Simulate app - sleep(3) - - if not self.should_capture_pokemon(pokemon_name, cp, pokemon_potential, response_dict): - return False - - flag_VIP = False - # @TODO, use the best ball in stock to catch VIP (Very Important Pokemon: Configurable) - if self.check_vip_pokemon(pokemon_name, cp, pokemon_potential): - self.emit_event( - 'vip_pokemon', - formatted='This is a VIP pokemon. Catch!!!' - ) - flag_VIP=True - - items_stock = self.bot.current_inventory() - berry_id = 701 # @ TODO: use better berries if possible - berries_count = self.bot.item_inventory_count(berry_id) - while True: - # pick the most simple ball from stock - pokeball = 1 # start from 1 - PokeBalls - berry_used = False - - if flag_VIP: - if(berries_count>0 and catch_rate[pokeball-1] < 0.9): - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - self.emit_event( - 'pokemon_catch_rate', - level='debug', - formatted="Catch rate of {catch_rate} is low. Maybe will throw {berry_name} ({berry_count} left)", - data={ - 'catch_rate': success_percentage, - 'berry_name': self.item_list[str(berry_id)], - 'berry_count': berries_count - } - ) - # Out of all pokeballs! Let's don't waste berry. - if items_stock[1] == 0 and items_stock[2] == 0 and items_stock[3] == 0: - break - - # Use the berry to catch - response_dict = self.api.use_item_capture( - item_id=berry_id, - encounter_id=encounter_id, - spawn_point_id=self.spawn_point_guid - ) - if response_dict and response_dict['status_code'] is 1 and 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: - for i in range(len(catch_rate)): - if 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: - catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - berries_count = berries_count -1 - berry_used = True - self.emit_event( - 'threw_berry', - formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", - data={ - "berry_name": self.item_list[str(berry_id)], - "new_catch_rate": success_percentage - } - ) - else: - if response_dict['status_code'] is 1: - self.emit_event( - 'softban', - level='warning', - formatted='Failed to use berry. You may be softbanned.' - ) - else: - self.emit_event( - 'threw_berry_failed', - formatted='Unknown response when throwing berry: {status_code}.', - data={ - 'status_code': response_dict['status_code'] - } - ) - - #use the best ball to catch - current_type = pokeball - #debug use normal ball - while current_type < 3: - current_type += 1 - if catch_rate[pokeball-1] < 0.9 and items_stock[current_type] > 0: - # if current ball chance to catch is under 90%, and player has better ball - then use it - pokeball = current_type # use better ball - else: - # If we have a lot of berries (than the great ball), we prefer use a berry first! - if catch_rate[pokeball-1] < 0.42 and items_stock[pokeball+1]+30 < berries_count: - # If it's not the VIP type, we don't want to waste our ultra ball if no balls left. - if items_stock[1] == 0 and items_stock[2] == 0: - break - - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - self.emit_event( - 'pokemon_catch_rate', - level='debug', - formatted="Catch rate of {catch_rate} is low. Maybe will throw {berry_name} ({berry_count} left)", - data={ - 'catch_rate': success_percentage, - 'berry_name': self.item_list[str(berry_id)], - 'berry_count': berries_count-1 - } - ) - response_dict = self.api.use_item_capture(item_id=berry_id, - encounter_id=encounter_id, - spawn_point_id=self.spawn_point_guid - ) - if response_dict and response_dict['status_code'] is 1 and 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: - for i in range(len(catch_rate)): - if 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: - catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - berries_count = berries_count -1 - berry_used = True - self.emit_event( - 'threw_berry', - formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", - data={ - "berry_name": self.item_list[str(berry_id)], - "new_catch_rate": success_percentage - } - ) - else: - if response_dict['status_code'] is 1: - self.emit_event( - 'softban', - level='warning', - formatted='Failed to use berry. You may be softbanned.' - ) - else: - self.emit_event( - 'threw_berry_failed', - formatted='Unknown response when throwing berry: {status_code}.', - data={ - 'status_code': response_dict['status_code'] - } - ) - - else: - #We don't have many berry to waste, pick a good ball first. Save some berry for future VIP pokemon - current_type = pokeball - while current_type < 2: - current_type += 1 - if catch_rate[pokeball-1] < 0.35 and items_stock[current_type] > 0: - # if current ball chance to catch is under 35%, and player has better ball - then use it - pokeball = current_type # use better ball - - #if the rate is still low and we didn't throw a berry before use berry - if catch_rate[pokeball-1] < 0.35 and berries_count > 0 and berry_used == False: - # If it's not the VIP type, we don't want to waste our ultra ball if no balls left. - if items_stock[1] == 0 and items_stock[2] == 0: - break - - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - self.emit_event( - 'pokemon_catch_rate', - level='debug', - formatted="Catch rate of {catch_rate} is low. Throwing {berry_name} ({berry_count} left)", - data={ - 'catch_rate': success_percentage, - 'berry_name': self.item_list[str(berry_id)], - 'berry_count': berries_count-1 - } - ) - response_dict = self.api.use_item_capture(item_id=berry_id, - encounter_id=encounter_id, - spawn_point_id=self.spawn_point_guid - ) - if response_dict and response_dict['status_code'] is 1 and 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: - for i in range(len(catch_rate)): - if 'item_capture_mult' in response_dict['responses']['USE_ITEM_CAPTURE']: - catch_rate[i] = catch_rate[i] * response_dict['responses']['USE_ITEM_CAPTURE']['item_capture_mult'] - success_percentage = '{0:.2f}'.format(catch_rate[pokeball-1]*100) - berries_count = berries_count -1 - berry_used = True - self.emit_event( - 'threw_berry', - formatted="Threw a {berry_name}! Catch rate now: {new_catch_rate}", - data={ - "berry_name": self.item_list[str(berry_id)], - "new_catch_rate": success_percentage - } - ) - else: - if response_dict['status_code'] is 1: - self.emit_event( - 'softban', - level='warning', - formatted='Failed to use berry. You may be softbanned.' - ) - else: - self.emit_event( - 'threw_berry_failed', - formatted='Unknown response when throwing berry: {status_code}.', - data={ - 'status_code': response_dict['status_code'] - } - ) - - # Re-check if berry is used, find a ball for a good capture rate - current_type=pokeball - while current_type < 2: - current_type += 1 - if catch_rate[pokeball-1] < 0.35 and items_stock[current_type] > 0: - pokeball = current_type # use better ball - - # This is to avoid rare case that a berry has ben throwed <0.42 - # and still picking normal pokeball (out of stock) -> error - if items_stock[1] == 0 and items_stock[2] > 0: - pokeball = 2 - - # Add this logic to avoid Pokeball = 0, Great Ball = 0, Ultra Ball = X - # And this logic saves Ultra Balls if it's a weak trash pokemon - if catch_rate[pokeball-1]<0.30 and items_stock[3]>0: - pokeball = 3 - - items_stock[pokeball] -= 1 - success_percentage = '{0:.2f}'.format(catch_rate[pokeball - 1] * 100) - self.emit_event( - 'threw_pokeball', - formatted='Used {pokeball}, with chance {success_percentage} ({count_left} left)', - data={ - 'pokeball': self.item_list[str(pokeball)], - 'success_percentage': success_percentage, - 'count_left': items_stock[pokeball] - } - ) - id_list1 = self.count_pokemon_inventory() - - reticle_size_parameter = normalized_reticle_size(self.config.catch_randomize_reticle_factor) - spin_modifier_parameter = spin_modifier(self.config.catch_randomize_spin_factor) - - response_dict = self.api.catch_pokemon( - encounter_id=encounter_id, - pokeball=pokeball, - normalized_reticle_size=reticle_size_parameter, - spawn_point_id=self.spawn_point_guid, - hit_pokemon=1, - spin_modifier=spin_modifier_parameter, - normalized_hit_position=1 - ) - - if response_dict and \ - 'responses' in response_dict and \ - 'CATCH_POKEMON' in response_dict['responses'] and \ - 'status' in response_dict['responses']['CATCH_POKEMON']: - status = response_dict['responses'][ - 'CATCH_POKEMON']['status'] - if status is 2: - self.emit_event( - 'pokemon_escaped', - formatted="{pokemon} escaped.", - data={'pokemon': pokemon_name} - ) - sleep(2) - continue - if status is 3: - self.emit_event( - 'pokemon_vanished', - formatted="{pokemon} vanished!", - data={'pokemon': pokemon_name} - ) - if success_percentage == 100: - self.softban = True - if status is 1: - self.bot.metrics.captured_pokemon(pokemon_name, cp, iv_display, pokemon_potential) - - self.emit_event( - 'pokemon_caught', - formatted='Captured {pokemon}! [CP {cp}] [Potential {iv}] [{iv_display}] [+{exp} exp]', - data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': pokemon_potential, - 'iv_display': iv_display, - 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) - } - ) - self.bot.softban = False - - if (self.config.evolve_captured - and (self.config.evolve_captured[0] == 'all' - or pokemon_name in self.config.evolve_captured)): - id_list2 = self.count_pokemon_inventory() - # No need to capture this even for metrics, player stats includes it. - pokemon_to_transfer = list(set(id_list2) - set(id_list1)) - - # TODO dont throw RuntimeError, do something better - if len(pokemon_to_transfer) == 0: - raise RuntimeError( - 'Trying to evolve 0 pokemons!') - response_dict = self.api.evolve_pokemon(pokemon_id=pokemon_to_transfer[0]) - status = response_dict['responses']['EVOLVE_POKEMON']['result'] - if status == 1: - self.emit_event( - 'pokemon_evolved', - formatted="{pokemon} evolved!", - data={'pokemon': pokemon_name} - ) - else: - self.emit_event( - 'pokemon_evolve_fail', - formatted="Failed to evolve {pokemon}!", - data={'pokemon': pokemon_name} - ) - break - time.sleep(5) - - def count_pokemon_inventory(self): - # don't use cached bot.get_inventory() here - # because we need to have actual information in capture logic - response_dict = self.api.get_inventory() - - id_list = [] - callback = lambda pokemon: id_list.append(pokemon['id']) - self._foreach_pokemon_in_inventory(response_dict, callback) - return id_list - - def _foreach_pokemon_in_inventory(self, response_dict, callback): + return WorkerResult.ERROR try: - reduce(dict.__getitem__, [ - "responses", "GET_INVENTORY", "inventory_delta", "inventory_items"], response_dict) + responses = response_dict['responses'] + response = responses[self.response_key] + if response[self.response_status_key] != ENCOUNTER_STATUS_SUCCESS: + if response[self.response_status_key] == ENCOUNTER_STATUS_NOT_IN_RANGE: + self.emit_event('pokemon_not_in_range', formatted='Pokemon went out of range!') + elif response[self.response_status_key] == ENCOUNTER_STATUS_POKEMON_INVENTORY_FULL: + self.emit_event('pokemon_inventory_full', formatted='Your Pokemon inventory is full! Could not catch!') + return WorkerResult.ERROR except KeyError: - pass - else: - for item in response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data"], item) - except KeyError: - pass - else: - pokemon = item['inventory_item_data']['pokemon_data'] - if not pokemon.get('is_egg', False): - callback(pokemon) - - def pokemon_potential(self, pokemon_data): - total_iv = 0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - - for individual_stat in iv_stats: - try: - total_iv += pokemon_data[individual_stat] - except: - pokemon_data[individual_stat] = 0 - continue - - return round((total_iv / 45.0), 2) - - def should_capture_pokemon(self, pokemon_name, cp, iv, response_dict): - catch_config = self._get_catch_config_for(pokemon_name) - cp_iv_logic = catch_config.get('logic') - if not cp_iv_logic: - cp_iv_logic = self._get_catch_config_for('any').get('logic', 'and') - - catch_results = { - 'cp': False, - 'iv': False, - } - - if catch_config.get('never_catch', False): - return False - - if catch_config.get('always_catch', False): - return True - - catch_cp = catch_config.get('catch_above_cp', 0) - if cp > catch_cp: - catch_results['cp'] = True - - catch_iv = catch_config.get('catch_above_iv', 0) - if iv > catch_iv: - catch_results['iv'] = True - - logic_to_function = { - 'or': lambda x, y: x or y, - 'and': lambda x, y: x and y - } - - return logic_to_function[cp_iv_logic](*catch_results.values()) + return WorkerResult.ERROR + + # get pokemon data + pokemon_data = response['wild_pokemon']['pokemon_data'] if 'wild_pokemon' in response else response['pokemon_data'] + pokemon = Pokemon(self.pokemon_list, pokemon_data) + + # skip ignored pokemon + if not self._should_catch_pokemon(pokemon): + return WorkerResult.SUCCESS + + # log encounter + self.emit_event( + 'pokemon_appeared', + formatted='A wild {pokemon} appeared! [CP {cp}] [Potential {iv}] [A/D/S {iv_display}]', + data={ + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv, + 'iv_display': pokemon.iv_display, + } + ) + + # simulate app + sleep(3) + + # check for VIP pokemon + is_vip = self._is_vip_pokemon(pokemon) + if is_vip: + self.emit_event('vip_pokemon', formatted='This is a VIP pokemon. Catch!!!') + + # catch that pokemon! + encounter_id = self.pokemon['encounter_id'] + catch_rate_by_ball = [0] + response['capture_probability']['capture_probability'] # offset so item ids match indces + self._do_catch(pokemon, encounter_id, catch_rate_by_ball, is_vip=is_vip) - def _get_catch_config_for(self, pokemon): - catch_config = self.config.catch.get(pokemon) - if not catch_config: - catch_config = self.config.catch.get('any') - return catch_config + # simulate app + time.sleep(5) def create_encounter_api_call(self): encounter_id = self.pokemon['encounter_id'] @@ -491,29 +147,288 @@ def create_encounter_api_call(self): ) return request.call() - def check_vip_pokemon(self,pokemon, cp, iv): + ############################################################################ + # helpers + ############################################################################ + + def _pokemon_matches_config(self, config, pokemon, default_logic='and'): + pokemon_config = config.get(pokemon.name, config.get('any')) + + if not pokemon_config: + return False - vip_name = self.config.vips.get(pokemon) - if vip_name == {}: - return True - else: - catch_config = self.config.vips.get("any") - if not catch_config: - return False - cp_iv_logic = catch_config.get('logic', 'or') catch_results = { 'cp': False, 'iv': False, } - catch_cp = catch_config.get('catch_above_cp', 0) - if cp > catch_cp: + if pokemon_config.get('never_catch', False): + return False + + if pokemon_config.get('always_catch', False): + return True + + catch_cp = pokemon_config.get('catch_above_cp', 0) + if pokemon.cp > catch_cp: catch_results['cp'] = True - catch_iv = catch_config.get('catch_above_iv', 0) - if iv > catch_iv: + + catch_iv = pokemon_config.get('catch_above_iv', 0) + if pokemon.iv > catch_iv: catch_results['iv'] = True - logic_to_function = { - 'or': lambda x, y: x or y, - 'and': lambda x, y: x and y - } - return logic_to_function[cp_iv_logic](*catch_results.values()) + + return LOGIC_TO_FUNCTION[pokemon_config.get('logic', default_logic)](*catch_results.values()) + + def _should_catch_pokemon(self, pokemon): + return self._pokemon_matches_config(self.config.catch, pokemon) + + def _is_vip_pokemon(self, pokemon): + # having just a name present in the list makes them vip + if self.config.vips.get(pokemon.name) == {}: + return True + return self._pokemon_matches_config(self.config.vips, pokemon, default_logic='or') + + def _get_current_pokemon_ids(self): + # don't use cached bot.get_inventory() here because we need to have actual information in capture logic + response_dict = self.api.get_inventory() + + try: + inventory_items = response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] + except KeyError: + return [] # no items + + id_list = [] + for item in inventory_items: + try: + pokemon = item['inventory_item_data']['pokemon_data'] + except KeyError: + continue + + # ignore eggs + if pokemon.get('is_egg'): + continue + + id_list.append(pokemon['id']) + + return id_list + + def _pct(self, rate_by_ball): + return '{0:.2f}'.format(rate_by_ball * 100) + + def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball): + new_catch_rate_by_ball = [] + self.emit_event( + 'pokemon_catch_rate', + level='debug', + formatted='Catch rate of {catch_rate} with {ball_name} is low. Throwing {berry_name} (have {berry_count})', + data={ + 'catch_rate': self._pct(catch_rate_by_ball[current_ball]), + 'ball_name': self.item_list[str(current_ball)], + 'berry_name': self.item_list[str(berry_id)], + 'berry_count': berry_count + } + ) + + response_dict = self.api.use_item_capture( + item_id=berry_id, + encounter_id=encounter_id, + spawn_point_id=self.spawn_point_guid + ) + responses = response_dict['responses'] + + if response_dict and response_dict['status_code'] == 1: + + # update catch rates using multiplier + if 'item_capture_mult' in responses['USE_ITEM_CAPTURE']: + for rate in catch_rate_by_ball: + new_catch_rate_by_ball.append(rate * responses['USE_ITEM_CAPTURE']['item_capture_mult']) + self.emit_event( + 'threw_berry', + formatted="Threw a {berry_name}! Catch rate with {ball_name} is now: {new_catch_rate}", + data={ + 'berry_name': self.item_list[str(berry_id)], + 'ball_name': self.item_list[str(current_ball)], + 'new_catch_rate': self._pct(catch_rate_by_ball[current_ball]) + } + ) + + # softban? + else: + new_catch_rate_by_ball = catch_rate_by_ball + self.emit_event( + 'softban', + level='warning', + formatted='Failed to use berry. You may be softbanned.' + ) + + # unknown status code + else: + new_catch_rate_by_ball = catch_rate_by_ball + self.emit_event( + 'threw_berry_failed', + formatted='Unknown response when throwing berry: {status_code}.', + data={ + 'status_code': response_dict['status_code'] + } + ) + + return new_catch_rate_by_ball + + def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): + # settings that may be exposed at some point + berry_id = ITEM_RAZZBERRY + maximum_ball = ITEM_ULTRABALL if is_vip else ITEM_GREATBALL + ideal_catch_rate_before_throw = 0.9 if is_vip else 0.35 + + berry_count = self.bot.item_inventory_count(berry_id) + items_stock = self.bot.current_inventory() + + while True: + + # find lowest available ball + current_ball = ITEM_POKEBALL + while items_stock[current_ball] == 0 and current_ball < maximum_ball: + current_ball += 1 + if items_stock[current_ball] == 0: + self.emit_event('no_pokeballs', formatted='No usable pokeballs found!') + break + + # check future ball count + num_next_balls = 0 + next_ball = current_ball + while next_ball < maximum_ball: + next_ball += 1 + num_next_balls += items_stock[next_ball] + + # check if we've got berries to spare + berries_to_spare = berry_count > 0 if is_vip else berry_count > num_next_balls + 30 + + # use a berry if we are under our ideal rate and have berries to spare + used_berry = False + if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and berries_to_spare: + catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) + berry_count -= 1 + used_berry = True + + # pick the best ball to catch with + best_ball = current_ball + while best_ball < maximum_ball: + best_ball += 1 + if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and items_stock[best_ball] > 0: + # if current ball chance to catch is under our ideal rate, and player has better ball - then use it + current_ball = best_ball + + # if the rate is still low and we didn't throw a berry before, throw one + if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and berry_count > 0 and not used_berry: + catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) + berry_count -= 1 + + # get current pokemon list before catch + pokemon_before_catch = self._get_current_pokemon_ids() + + # try to catch pokemon! + items_stock[current_ball] -= 1 + self.emit_event( + 'threw_pokeball', + formatted='Used {ball_name}, with chance {success_percentage} ({count_left} left)', + data={ + 'ball_name': self.item_list[str(current_ball)], + 'success_percentage': self._pct(catch_rate_by_ball[current_ball]), + 'count_left': items_stock[current_ball] + } + ) + + reticle_size_parameter = normalized_reticle_size(self.config.catch_randomize_reticle_factor) + spin_modifier_parameter = spin_modifier(self.config.catch_randomize_spin_factor) + + response_dict = self.api.catch_pokemon( + encounter_id=encounter_id, + pokeball=current_ball, + normalized_reticle_size=reticle_size_parameter, + spawn_point_id=self.spawn_point_guid, + hit_pokemon=1, + spin_modifier=spin_modifier_parameter, + normalized_hit_position=1 + ) + + try: + catch_pokemon_status = response_dict['responses']['CATCH_POKEMON']['status'] + except KeyError: + break + + # retry failed pokemon + if catch_pokemon_status == CATCH_STATUS_FAILED: + self.emit_event( + 'pokemon_capture_failed', + formatted='{pokemon} capture failed.. trying again!', + data={'pokemon': pokemon.name} + ) + sleep(2) + continue + + # abandon if pokemon vanished + elif catch_pokemon_status == CATCH_STATUS_VANISHED: + self.emit_event( + 'pokemon_vanished', + formatted='{pokemon} vanished!', + data={'pokemon': pokemon.name} + ) + if self._pct(catch_rate_by_ball[current_ball]) == 100: + self.bot.softban = True + + # pokemon caught! + elif catch_pokemon_status == CATCH_STATUS_SUCCESS: + self.bot.metrics.captured_pokemon(pokemon.name, pokemon.cp, pokemon.iv_display, pokemon.iv) + self.emit_event( + 'pokemon_caught', + formatted='Captured {pokemon}! [CP {cp}] [Potential {iv}] [{iv_display}] [+{exp} exp]', + data={ + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv, + 'iv_display': pokemon.iv_display, + 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) + } + ) + + # We could refresh here too, but adding 3 saves a inventory request + candy = inventory.candies().get(pokemon.num) + candy.add(3) + self.emit_event( + 'gained_candy', + formatted='You now have {quantity} {type} candy!', + data = { + 'quantity': candy.quantity, + 'type': candy.type, + }, + ) + + self.bot.softban = False + + # evolve pokemon if necessary + if self.config.evolve_captured and (self.config.evolve_captured[0] == 'all' or pokemon.name in self.config.evolve_captured): + pokemon_after_catch = self._get_current_pokemon_ids() + pokemon_to_evolve = list(set(pokemon_after_catch) - set(pokemon_before_catch)) + + if len(pokemon_to_evolve) == 0: + break + + self._do_evolve(pokemon, pokemon_to_evolve[0]) + + break + + def _do_evolve(self, pokemon, new_pokemon_id): + response_dict = self.api.evolve_pokemon(pokemon_id=new_pokemon_id) + catch_pokemon_status = response_dict['responses']['EVOLVE_POKEMON']['result'] + + if catch_pokemon_status == 1: + self.emit_event( + 'pokemon_evolved', + formatted='{pokemon} evolved!', + data={'pokemon': pokemon.name} + ) + else: + self.emit_event( + 'pokemon_evolve_fail', + formatted='Failed to evolve {pokemon}!', + data={'pokemon': pokemon.name} + ) diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 2c969913b0..673b373fba 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -3,10 +3,12 @@ from pokemongo_bot.base_task import BaseTask from pokemongo_bot.tree_config_builder import ConfigException + class RecycleItems(BaseTask): SUPPORTED_TASK_API_VERSION = 1 def initialize(self): + self.min_empty_space = self.config.get('min_empty_space', None) self.item_filter = self.config.get('item_filter', {}) self._validate_item_filter() @@ -15,9 +17,26 @@ def _validate_item_filter(self): for config_item_name, bag_count in self.item_filter.iteritems(): if config_item_name not in item_list.viewvalues(): if config_item_name not in item_list: - raise ConfigException("item {} does not exist, spelling mistake? (check for valid item names in data/items.json)".format(config_item_name)) + raise ConfigException( + "item {} does not exist, spelling mistake? (check for valid item names in data/items.json)".format( + config_item_name)) def work(self): + items_in_bag = self.bot.get_inventory_count('item') + total_bag_space = self.bot.player_data['max_item_storage'] + free_bag_space = total_bag_space - items_in_bag + + if self.min_empty_space is not None: + if free_bag_space >= self.min_empty_space: + self.emit_event( + 'item_discard_skipped', + formatted="Skipping Recycling of Items. {space} space left in bag.", + data={ + 'space': free_bag_space + } + ) + return + self.bot.latest_inventory = None item_count_dict = self.bot.item_inventory_count('all') @@ -58,7 +77,7 @@ def work(self): def send_recycle_item_request(self, item_id, count): # Example of good request response - #{'responses': {'RECYCLE_INVENTORY_ITEM': {'result': 1, 'new_count': 46}}, 'status_code': 1, 'auth_ticket': {'expire_timestamp_ms': 1469306228058L, 'start': '/HycFyfrT4t2yB2Ij+yoi+on778aymMgxY6RQgvrGAfQlNzRuIjpcnDd5dAxmfoTqDQrbz1m2dGqAIhJ+eFapg==', 'end': 'f5NOZ95a843tgzprJo4W7Q=='}, 'request_id': 8145806132888207460L} + # {'responses': {'RECYCLE_INVENTORY_ITEM': {'result': 1, 'new_count': 46}}, 'status_code': 1, 'auth_ticket': {'expire_timestamp_ms': 1469306228058L, 'start': '/HycFyfrT4t2yB2Ij+yoi+on778aymMgxY6RQgvrGAfQlNzRuIjpcnDd5dAxmfoTqDQrbz1m2dGqAIhJ+eFapg==', 'end': 'f5NOZ95a843tgzprJo4W7Q=='}, 'request_id': 8145806132888207460L} return self.bot.api.recycle_inventory_item( item_id=item_id, count=count diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index e04a86dbc6..445946e7e1 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -15,14 +15,16 @@ class SpinFort(BaseTask): SUPPORTED_TASK_API_VERSION = 1 + def initialize(self): + self.ignore_item_count = self.config.get("ignore_item_count", False) + def should_run(self): if not self.bot.has_space_for_loot(): self.emit_event( 'inventory_full', - formatted="Not moving to any forts as there aren't enough space. You might want to change your config to recycle more items if this message appears consistently." + formatted="Inventory is full. You might want to change your config to recycle more items if this message appears consistently." ) - return False - return True + return self.ignore_item_count or self.bot.has_space_for_loot() def work(self): fort = self.get_fort_in_range() @@ -139,6 +141,11 @@ def work(self): def get_fort_in_range(self): forts = self.bot.get_forts(order_by_distance=True) + for fort in forts: + if 'cooldown_complete_timestamp_ms' in fort: + self.bot.fort_timeouts[fort["id"]] = fort['cooldown_complete_timestamp_ms'] + forts.remove(fort) + forts = filter(lambda x: x["id"] not in self.bot.fort_timeouts, forts) if len(forts) == 0: diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 5c1d30fae7..ebc197ef24 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -43,17 +43,6 @@ def work(self): all_pokemons.remove(pokemon) best_pokemons.append(pokemon) - if best_pokemons and all_pokemons: - self.emit_event( - 'keep_best_release', - formatted="Keeping best {amount} {pokemon}, based on {criteria}", - data={ - 'amount': len(best_pokemons), - 'pokemon': pokemon_name, - 'criteria': order_criteria - } - ) - transfer_pokemons = [pokemon for pokemon in all_pokemons if self.should_release_pokemon(pokemon_name, pokemon['cp'], @@ -61,6 +50,16 @@ def work(self): True)] if transfer_pokemons: + if best_pokemons: + self.emit_event( + 'keep_best_release', + formatted="Keeping best {amount} {pokemon}, based on {criteria}", + data={ + 'amount': len(best_pokemons), + 'pokemon': pokemon_name, + 'criteria': order_criteria + } + ) for pokemon in transfer_pokemons: self.release_pokemon(pokemon_name, pokemon['cp'], pokemon['iv'], pokemon['pokemon_data']['id']) else: diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index acbfaa7fe4..0a6e3c592d 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -70,6 +70,8 @@ def __init__(self, bot, config): self.min_interval = self.DEFAULT_MIN_INTERVAL self.displayed_stats = self.DEFAULT_DISPLAYED_STATS + self.bot.event_manager.register_event('update_title', parameters=('title')) + self._process_config() def initialize(self): @@ -109,6 +111,15 @@ def _update_title(self, title, platform): :rtype: None :raise: RuntimeError: When the given platform isn't supported. """ + + self.emit_event( + 'update_title', + formatted="{title}", + data={ + 'title': title + } + ) + if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) stdout.flush() diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py new file mode 100644 index 0000000000..c74a85296f --- /dev/null +++ b/pokemongo_bot/inventory.py @@ -0,0 +1,251 @@ +import json +import os + +''' +Helper class for updating/retrieving Inventory data +''' + +class _BaseInventoryComponent(object): + TYPE = None # base key name for items of this type + ID_FIELD = None # identifier field for items of this type + STATIC_DATA_FILE = None # optionally load static data from file, + # dropping the data in a static variable named STATIC_DATA + + def __init__(self): + self._data = {} + if self.STATIC_DATA_FILE is not None: + self.init_static_data() + + @classmethod + def init_static_data(cls): + if not hasattr(cls, 'STATIC_DATA') or cls.STATIC_DATA is None: + cls.STATIC_DATA = json.load(open(cls.STATIC_DATA_FILE)) + + def parse(self, item): + # optional hook for parsing the dict for this item + # default is to use the dict directly + return item + + def retrieve_data(self, inventory): + assert self.TYPE is not None + assert self.ID_FIELD is not None + ret = {} + for item in inventory: + data = item['inventory_item_data'] + if self.TYPE in data: + item = data[self.TYPE] + key = item[self.ID_FIELD] + ret[key] = self.parse(item) + return ret + + def refresh(self, inventory): + self._data = self.retrieve_data(inventory) + + def get(self, id): + return self._data(id) + + def all(self): + return list(self._data.values()) + + +class Candy(object): + def __init__(self, family_id, quantity): + self.type = Pokemons.name_for(family_id) + self.quantity = quantity + + def consume(self, amount): + if self.quantity < amount: + raise Exception('Tried to consume more {} candy than you have'.format(self.type)) + self.quantity -= amount + + def add(self, amount): + if amount < 0: + raise Exception('Must add positive amount of candy') + self.quantity += amount + +class Candies(_BaseInventoryComponent): + TYPE = 'candy' + ID_FIELD = 'family_id' + + @classmethod + def family_id_for(self, pokemon_id): + return Pokemons.first_evolution_id_for(pokemon_id) + + def get(self, pokemon_id): + family_id = self.family_id_for(pokemon_id) + return self._data.setdefault(family_id, Candy(family_id, 0)) + + def parse(self, item): + candy = item['candy'] if 'candy' in item else 0 + return Candy(item['family_id'], candy) + + +class Pokedex(_BaseInventoryComponent): + TYPE = 'pokedex_entry' + ID_FIELD = 'pokemon_id' + + def seen(self, pokemon_id): + return pokemon_id in self._data + + def captured(self, pokemon_id): + if not self.seen(pokemon_id): + return False + return self._data[pokemon_id]['times_captured'] > 0 + + +class Items(_BaseInventoryComponent): + TYPE = 'item' + ID_FIELD = 'item_id' + STATIC_DATA_FILE = os.path.join('data', 'items.json') + + def count_for(self, item_id): + return self._data[item_id]['count'] + + +class Pokemons(_BaseInventoryComponent): + TYPE = 'pokemon_data' + ID_FIELD = 'id' + STATIC_DATA_FILE = os.path.join('data', 'pokemon.json') + + def parse(self, item): + if 'is_egg' in item: + return Egg(item) + return Pokemon(item) + + @classmethod + def data_for(cls, pokemon_id): + return cls.STATIC_DATA[pokemon_id - 1] + + @classmethod + def name_for(cls, pokemon_id): + return cls.data_for(pokemon_id)['Name'] + + @classmethod + def first_evolution_id_for(cls, pokemon_id): + data = cls.data_for(pokemon_id) + if 'Previous evolution(s)' in data: + return int(data['Previous evolution(s)'][0]['Number']) + return pokemon_id + + @classmethod + def next_evolution_id_for(cls, pokemon_id): + try: + return int(cls.data_for(pokemon_id)['Next evolution(s)'][0]['Number']) + except KeyError: + return None + + @classmethod + def evolution_cost_for(cls, pokemon_id): + try: + return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + except KeyError: + return + + def all(self): + # by default don't include eggs in all pokemon (usually just + # makes caller's lives more difficult) + return [p for p in super(Pokemons, self).all() if not isinstance(p, Egg)] + +class Egg(object): + def __init__(self, data): + self._data = data + + def has_next_evolution(self): + return False + + +class Pokemon(object): + def __init__(self, data): + self._data = data + self.id = data['id'] + self.pokemon_id = data['pokemon_id'] + self.cp = data['cp'] + self._static_data = Pokemons.data_for(self.pokemon_id) + self.name = Pokemons.name_for(self.pokemon_id) + self.iv = self._compute_iv() + + def can_evolve_now(self): + return self.has_next_evolution() and self.candy_quantity > self.evolution_cost + + def has_next_evolution(self): + return 'Next Evolution Requirements' in self._static_data + + def has_seen_next_evolution(self): + return pokedex().captured(self.next_evolution_id) + + @property + def next_evolution_id(self): + return Pokemons.next_evolution_id_for(self.pokemon_id) + + @property + def first_evolution_id(self): + return Pokemons.first_evolution_id_for(self.pokemon_id) + + @property + def candy_quantity(self): + return candies().get(self.pokemon_id).quantity + + @property + def evolution_cost(self): + return self._static_data['Next Evolution Requirements']['Amount'] + + def _compute_iv(self): + total_IV = 0.0 + iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] + + for individual_stat in iv_stats: + try: + total_IV += self._data[individual_stat] + except Exception: + self._data[individual_stat] = 0 + continue + pokemon_potential = round((total_IV / 45.0), 2) + return pokemon_potential + + +class Inventory(object): + def __init__(self, bot): + self.bot = bot + self.pokedex = Pokedex() + self.candy = Candies() + self.items = Items() + self.pokemons = Pokemons() + self.refresh() + + def refresh(self): + # TODO: it would be better if this class was used for all + # inventory management. For now, I'm just clearing the old inventory field + self.bot.latest_inventory = None + inventory = self.bot.get_inventory()['responses']['GET_INVENTORY'][ + 'inventory_delta']['inventory_items'] + for i in (self.pokedex, self.candy, self.items, self.pokemons): + i.refresh(inventory) + + +_inventory = None + +def init_inventory(bot): + global _inventory + _inventory = Inventory(bot) + + +def refresh_inventory(): + _inventory.refresh() + + +def pokedex(): + return _inventory.pokedex + + +def candies(refresh=False): + if refresh: + refresh_inventory() + return _inventory.candy + + +def pokemons(): + return _inventory.pokemons + + +def items(): + return _inventory.items diff --git a/pokemongo_bot/step_walker.py b/pokemongo_bot/step_walker.py index 263699095d..f6c2cbfe96 100644 --- a/pokemongo_bot/step_walker.py +++ b/pokemongo_bot/step_walker.py @@ -39,6 +39,7 @@ def __init__(self, bot, speed, dest_lat, dest_lng): def step(self): if (self.dLat == 0 and self.dLng == 0) or self.dist < self.speed: self.api.set_position(self.destLat, self.destLng, 0) + self.bot.heartbeat() return True totalDLat = (self.destLat - self.initLat) diff --git a/pokemongo_bot/worker_result.py b/pokemongo_bot/worker_result.py index f38ceb9704..0e3ba10ebd 100644 --- a/pokemongo_bot/worker_result.py +++ b/pokemongo_bot/worker_result.py @@ -1,3 +1,4 @@ class WorkerResult(object): RUNNING = 'RUNNING' SUCCESS = 'SUCCESS' + ERROR = 'ERROR' diff --git a/requirements.txt b/requirements.txt index f6a22a0233..76b1d15a7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy==1.11.0 networkx==1.11 --e git+https://github.com/keyphact/pgoapi.git@249d3be7fbbdabc7f9adea17cbc899d6549e47a2#egg=pgoapi +-e git+https://github.com/keyphact/pgoapi.git@a2755eb42dfe49e359798d2f4defefc97fb8163d#egg=pgoapi geopy==1.11.0 protobuf==3.0.0b4 requests==2.10.0 diff --git a/run.sh b/run.sh index ec95acb3e3..0812dfac8c 100755 --- a/run.sh +++ b/run.sh @@ -1,18 +1,21 @@ #!/usr/bin/env bash - -# Starts PokemonGo-Bot -config="" - +pokebotpath=$(pwd) +filename="" if [ ! -z $1 ]; then - config=$1 +filename=$1 else - config="./configs/config.json" - if [ ! -f ${config} ]; then - echo -e "There's no ./configs/config.json file" - echo -e "Please create one or use another config file" - echo -e "./run.sh [path/to/config/file]" - exit 1 - fi +filename="./configs/config.json" +if [ ! -f "$filename" ] +then +echo "There's no "$filename" file. use setup.sh -config to creat one." +fi fi -python pokecli.py --config ${config} +while true +do +cd $pokebotpath +python pokecli.py -cf $filename +read -p "Press any button or wait 20 seconds." -r -s -n1 -t 20 +echo `date`"Pokebot"$*" Stopped." +done +exit 0 diff --git a/setup.py b/setup.py deleted file mode 100644 index 6ccf893c0d..0000000000 --- a/setup.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python - -from pip.req import parse_requirements - -install_reqs = parse_requirements("requirements.txt", session=False) - -reqs = [str(ir.req) for ir in install_reqs] - -setup(name='pgoapi', - version='1.0', - url='https://github.com/tejado/pgoapi', - packages=['pgoapi'], - install_requires=reqs) diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000000..bcff0feefb --- /dev/null +++ b/setup.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +pokebotpath=$(pwd) +backuppath=$(pwd)"/backup" + +function Pokebotupdate () { +cd $pokebotpath +git pull +git submodule init +git submodule foreach git pull origin master +virtualenv . +source bin/activate +pip install -r requirements.txt +} + +function Pokebotencrypt () { +echo "Start to make encrypt.so" +wget http://pgoapi.com/pgoencrypt.tar.gz +tar -xf pgoencrypt.tar.gz +cd pgoencrypt/src/ +make +mv libencrypt.so $pokebotpath/encrypt.so +cd ../.. +rm -rf pgoencrypt.tar.gz +rm -rf pgoencrypt +} + +function Pokebotconfig () { +cd $pokebotpath +read -p "1.google 2.ptc +" auth +read -p "Input username +" username +read -p "Input password +" -s password +read -p " +Input location +" location +read -p "Input gmapkey +" gmapkey +cp configs/config.json.example configs/config.json +if [ "$auth" = "2" ] +then +sed -i "s/google/ptc/g" configs/config.json +fi +sed -i "s/YOUR_USERNAME/$username/g" configs/config.json +sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json +sed -i "s/SOME_LOCATION/$location/g" configs/config.json +sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json +echo "Edit configs/config.json to modify any other config." +} + +function Pokebotinstall () { +cd $pokebotpath +if [ -f /etc/debian_version ] +then +echo "You are on Debian/Ubuntu" +sudo apt-get update +sudo apt-get -y install python python-pip python-dev build-essential git python-protobuf virtualenv +elif [ -f /etc/redhat-release ] +then +echo "You are on CentOS/RedHat" +sudo yum -y install epel-release +sudo yum -y install python-pip +elif [ "$(uname -s)" == "Darwin" ] +then +echo "You are on Mac os" +sudo brew update +sudo brew install --devel protobuf +else +echo "Nothing happend." +fi +sudo pip install virtualenv +Pokebotupdate +Pokebotencrypt +echo "Install complete." +Pokebotconfig +} + +function Pokebotreset () { +cd $pokebotpath +git fetch --all +git reset --hard origin/dev +Pokebotupdate +} + +function Pokebothelp () { +echo "usage:" +echo " -i,--install. Install PokemonGo-Bot." +echo " -b,--backup. Backup config files." +echo " -c,--config. Easy config generator." +echo " -e,--encrypt. Make encrypt.so." +echo " -r,--reset. Force sync dev branch." +echo " -u,--update. Command git pull to update." +} + +case $* in +--install|-i) +Pokebotinstall +;; +--encrypt|-e) +Pokebotencrypt +;; +--reset|-r) +Pokebotreset +;; +--update|-u) +Pokebotupdate +;; +--backup|-b) +mkdir $backuppath +cp -f $pokebotpath/configs/config*.json $backuppath/ +cp -f $pokebotpath/web/config/userdata.js $backuppath/ +echo "Backup complete" +;; +--config|-c) +Pokebotconfig +;; +--help|-h) +Pokebothelp +;; +*.json) +filename=$* +cd $pokebotpath +if [ ! -f ./configs/"$filename" ] +then +echo "There's no ./configs/"$filename" file. It's better to use run.sh not this one." +else +Pokebotrun +fi +;; +*) +Pokebothelp +;; +esac +exit 0 From 234baa4331f5ddb66af9171b1b9c4184238178df Mon Sep 17 00:00:00 2001 From: mjmadsen Date: Tue, 9 Aug 2016 13:28:37 -0500 Subject: [PATCH 062/143] Checking config file exists in run.sh (#3326) --- run.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/run.sh b/run.sh index 0812dfac8c..d8406697e7 100755 --- a/run.sh +++ b/run.sh @@ -5,10 +5,10 @@ if [ ! -z $1 ]; then filename=$1 else filename="./configs/config.json" -if [ ! -f "$filename" ] -then -echo "There's no "$filename" file. use setup.sh -config to creat one." fi + +if [ ! -f "$filename" ]; then +echo "There's no "$filename" file. use setup.sh -config to creat one." fi while true From afb139d61fc911aa1f8df58dd6e858ce1c644c09 Mon Sep 17 00:00:00 2001 From: Amal Samally Date: Tue, 9 Aug 2016 22:33:33 +0400 Subject: [PATCH 063/143] Improve and update pokemon.json (#3331) 1. Unminify for simplier edits 2. Add BaseAttack, BaseDefense, BaseStamina, CaptureRate, FleeRate, Fast/Special Attack(s) --- data/pokemon.json | 5895 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 5894 insertions(+), 1 deletion(-) diff --git a/data/pokemon.json b/data/pokemon.json index a227106841..58f7c437f2 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -1 +1,5894 @@ -[{"Number":"001","Name":"Bulbasaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Tackle","Vine Whip"],"Weight":"6.9 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"002","Name":"Ivysaur"},{"Number":"003","Name":"Venusaur"}]},{"Number":"002","Name":"Ivysaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"13.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"003","Name":"Venusaur"}]},{"Number":"003","Name":"Venusaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"100.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"},{"Number":"002","Name":"Ivysaur"}]},{"Number":"004","Name":"Charmander","Classification":"Lizard Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Scratch"],"Weight":"8.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"005","Name":"Charmeleon"},{"Number":"006","Name":"Charizard"}]},{"Number":"005","Name":"Charmeleon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"19.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"}],"Next Evolution Requirements":{"Amount":100,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"006","Name":"Charizard"}]},{"Number":"006","Name":"Charizard","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Ember","Wing Attack"],"Weight":"90.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"},{"Number":"005","Name":"Charmeleon"}]},{"Number":"007","Name":"Squirtle","Classification":"Tiny Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Tackle","Bubble"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"008","Name":"Wartortle"},{"Number":"009","Name":"Blastoise"}]},{"Number":"008","Name":"Wartortle","Classification":"Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"22.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"}],"Next Evolution Requirements":{"Amount":100,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"009","Name":"Blastoise"}]},{"Number":"009","Name":"Blastoise","Classification":"Shellfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"85.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"},{"Number":"008","Name":"Wartortle"}]},{"Number":"010","Name":"Caterpie","Classification":"Worm Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"2.9 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"011","Name":"Metapod"},{"Number":"012","Name":"Butterfree"}]},{"Number":"011","Name":"Metapod","Classification":"Cocoon Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"9.9 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"}],"Next Evolution Requirements":{"Amount":50,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"012","Name":"Butterfree"}]},{"Number":"012","Name":"Butterfree","Classification":"Butterfly Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"32.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"},{"Number":"011","Name":"Metapod"}]},{"Number":"013","Name":"Weedle","Classification":"Hairy Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Sting"],"Weight":"3.2 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"014","Name":"Kakuna"},{"Number":"015","Name":"Beedrill"}]},{"Number":"014","Name":"Kakuna","Classification":"Cocoon Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Posion Sting"],"Weight":"10.0 kg","Height":"0.6 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"}],"Next Evolution Requirements":{"Amount":50,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"015","Name":"Beedrill"}]},{"Number":"015","Name":"Beedrill","Classification":"Poison Bee Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Jab"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"},{"Number":"014","Name":"Kakuna"}]},{"Number":"016","Name":"Pidgey","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"1.8 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"017","Name":"Pidgeotto"},{"Number":"018","Name":"Pidgeot"}]},{"Number":"017","Name":"Pidgeotto","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"30.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"}],"Next Evolution Requirements":{"Amount":50,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"018","Name":"Pidgeot"}]},{"Number":"018","Name":"Pidgeot","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Hurricane"],"Weight":"39.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"},{"Number":"017","Name":"Pidgeotto"}]},{"Number":"019","Name":"Rattata","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Body Slam","Dig","Hyper Fang"],"Weight":"3.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Rattata candies"},"Next evolution(s)":[{"Number":"020","Name":"Raticate"}]},{"Number":"020","Name":"Raticate","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Quick Attack"],"Special Attack(s)":["Dig","Hyper Beam","Hyper Fang"],"Weight":"18.5 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"019","Name":"Rattata"}]},{"Number":"021","Name":"Spearow","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"2.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Spearow candies"},"Next evolution(s)":[{"Number":"022","Name":"Fearow"}]},{"Number":"022","Name":"Fearow","Classification":"Beak Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Steel Wing"],"Weight":"38.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"021","Name":"Spearow"}]},{"Number":"023","Name":"Ekans","Classification":"Snake Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Sting"],"Weight":"6.9 kg","Height":"2.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ekans candies"},"Next evolution(s)":[{"Number":"024","Name":"Arbok"}]},{"Number":"024","Name":"Arbok","Classification":"Cobra Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Bite"],"Weight":"65.0 kg","Height":"3.5 m","Previous evolution(s)":[{"Number":"023","Name":"Ekans"}]},{"Number":"025","Name":"Pikachu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Quick Attack","Thunder Shock"],"Weight":"6.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Pikachu candies"},"Next evolution(s)":[{"Number":"026","Name":"Raichu"}]},{"Number":"026","Name":"Raichu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock","Spark"],"Weight":"30.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"025","Name":"Pikachu"}]},{"Number":"027","Name":"Sandshrew","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"12.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Sandshrew candies"},"Next evolution(s)":[{"Number":"028","Name":"Sandslash"}]},{"Number":"028","Name":"Sandslash","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"027","Name":"Sandshrew"}]},{"Number":"029","Name":"Nidoran F","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"7.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"030","Name":"Nidorina"},{"Number":"031","Name":"Nidoqueen"}]},{"Number":"030","Name":"Nidorina","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"20.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"031","Name":"Nidoqueen"}]},{"Number":"031","Name":"Nidoqueen","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"},{"Number":"030","Name":"Nidorina"}]},{"Number":"032","Name":"Nidoran M","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Peck","Poison Sting"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"033","Name":"Nidorino"},{"Number":"034","Name":"Nidoking"}]},{"Number":"033","Name":"Nidorino","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"19.5 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"034","Name":"Nidoking"}]},{"Number":"034","Name":"Nidoking","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Fury Cutter","Poison Jab"],"Weight":"62.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"},{"Number":"033","Name":"Nidorino"}]},{"Number":"035","Name":"Clefairy","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"7.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Clefairy candies"},"Next evolution(s)":[{"Number":"036","Name":"Clefable"}]},{"Number":"036","Name":"Clefable","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"40.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"035","Name":"Clefairy"}]},{"Number":"037","Name":"Vulpix","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"9.9 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Vulpix candies"},"Next evolution(s)":[{"Number":"038","Name":"Ninetales"}]},{"Number":"038","Name":"Ninetales","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"19.9 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"037","Name":"Vulpix"}]},{"Number":"039","Name":"Jigglypuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"5.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Jigglypuff candies"},"Next evolution(s)":[{"Number":"039","Name":"Jigglypuff"}]},{"Number":"040","Name":"Wigglytuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"12.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"040","Name":"Wigglytuff"}]},{"Number":"041","Name":"Zubat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Quick Attack"],"Weight":"7.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Zubat candies"},"Next evolution(s)":[{"Number":"042","Name":"Golbat"}]},{"Number":"042","Name":"Golbat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Wing Attack"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"041","Name":"Zubat"}]},{"Number":"043","Name":"Oddish","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"5.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"044","Name":"Gloom"},{"Number":"045","Name":"Vileplume"}]},{"Number":"044","Name":"Gloom","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"8.6 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"}],"Next Evolution Requirements":{"Amount":100,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"045","Name":"Vileplume"}]},{"Number":"045","Name":"Vileplume","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid",""],"Weight":"18.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"},{"Number":"044","Name":"Gloom"}]},{"Number":"046","Name":"Paras","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Scratch"],"Weight":"5.4 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Paras candies"},"Next evolution(s)":[{"Number":"047","Name":"Parasect"}]},{"Number":"047","Name":"Parasect","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Fury Cutter"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"046","Name":"Paras"}]},{"Number":"048","Name":"Venonat","Classification":"Insect Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Venonat candies"},"Next evolution(s)":[{"Number":"049","Name":"Venomoth"}]},{"Number":"049","Name":"Venomoth","Classification":"Poison Moth Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"12.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"048","Name":"Venonat"}]},{"Number":"050","Name":"Diglett","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"0.8 kg","Height":"0.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Diglett candies"},"Next evolution(s)":[{"Number":"051","Name":"Dugtrio"}]},{"Number":"051","Name":"Dugtrio","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Sucker Punch"],"Weight":"33.3 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"050","Name":"Diglett"}]},{"Number":"052","Name":"Meowth","Classification":"Scratch Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Scratch"],"Weight":"4.2 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Meowth candies"},"Next evolution(s)":[{"Number":"053","Name":"Persian"}]},{"Number":"053","Name":"Persian","Classification":"Classy Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Scratch"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"052","Name":"Meowth"}]},{"Number":"054","Name":"Psyduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun","Zen Headbutt"],"Weight":"19.6 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Psyduck candies"},"Next evolution(s)":[{"Number":"055","Name":"Golduck"}]},{"Number":"055","Name":"Golduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"76.6 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"054","Name":"Psyduck"}]},{"Number":"056","Name":"Mankey","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Scratch"],"Weight":"28.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Mankey candies"},"Next evolution(s)":[{"Number":"057","Name":"Primeape"}]},{"Number":"057","Name":"Primeape","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"056","Name":"Mankey"}]},{"Number":"058","Name":"Growlithe","Classification":"Puppy Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Ember"],"Weight":"19.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":50,"Name":"Growlithe candies"},"Next evolution(s)":[{"Number":"059","Name":"Arcanine"}]},{"Number":"059","Name":"Arcanine","Classification":"Legendary Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Fire Fang"],"Weight":"155.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"058","Name":"Growlithe"}]},{"Number":"060","Name":"Poliwag","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"12.4 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"061","Name":"Poliwhirl"},{"Number":"062","Name":"Poliwrath"}]},{"Number":"061","Name":"Poliwhirl","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"20.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"}],"Next Evolution Requirements":{"Amount":100,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"062","Name":"Poliwrath"}]},{"Number":"062","Name":"Poliwrath","Classification":"Tadpole Pokemon","Type I":["Water"],"Type II":["Fighting"],"Weaknesses":["Electric","Grass","Flying","Psychic","Fairy"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"54.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"},{"Number":"061","Name":"Poliwhirl"}]},{"Number":"063","Name":"Abra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Zen Headbutt",""],"Weight":"19.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":25,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"064","Name":"Kadabra"},{"Number":"065","Name":"Alakazam"}]},{"Number":"064","Name":"Kadabra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"56.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"}],"Next Evolution Requirements":{"Amount":100,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"065","Name":"Alakazam"}]},{"Number":"065","Name":"Alakazam","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"48.0 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"},{"Number":"064","Name":"Kadabra"}]},{"Number":"066","Name":"Machop","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"19.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"067","Name":"Machoke"},{"Number":"068","Name":"Machamp"}]},{"Number":"067","Name":"Machoke","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"70.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"}],"Next Evolution Requirements":{"Amount":100,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"068","Name":"Machamp"}]},{"Number":"068","Name":"Machamp","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Karate Chop"],"Weight":"130.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"},{"Number":"067","Name":"Machoke"}]},{"Number":"069","Name":"Bellsprout","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Vine Whip"],"Weight":"4.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"070","Name":"Weepinbell"},{"Number":"071","Name":"Victreebel"}]},{"Number":"070","Name":"Weepinbell","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"6.4 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"071","Name":"Victreebel"}]},{"Number":"071","Name":"Victreebel","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"15.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"},{"Number":"070","Name":"Weepinbell"}]},{"Number":"072","Name":"Tentacool","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Bubble","Poison Sting"],"Weight":"45.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Tentacool candies"},"Next evolution(s)":[{"Number":"073","Name":"Tentacruel"}]},{"Number":"073","Name":"Tentacruel","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Jab"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"072","Name":"Tentacool"}]},{"Number":"074","Name":"Geodude","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"20.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"075","Name":"Graveler"},{"Number":"076","Name":"Golem"}]},{"Number":"075","Name":"Graveler","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"105.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"}],"Next Evolution Requirements":{"Amount":100,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"076","Name":"Golem"}]},{"Number":"076","Name":"Golem","Classification":"Megaton Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"300.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"},{"Number":"075","Name":"Graveler"}]},{"Number":"077","Name":"Ponyta","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Tackle"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ponyta candies"},"Next evolution(s)":[{"Number":"078","Name":"Rapidash"}]},{"Number":"078","Name":"Rapidash","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Low Kick"],"Weight":"95.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"077","Name":"Ponyta"}]},{"Number":"079","Name":"Slowpoke","Classification":"Dopey Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"36.0 kg","Height":"1.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Slowpoke candies"},"Next evolution(s)":[{"Number":"080","Name":"Slowbro"}]},{"Number":"080","Name":"Slowbro","Classification":"Hermit Crab Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"78.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"079","Name":"Slowpoke"}]},{"Number":"081","Name":"Magnemite","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"6.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Magnemite candies"},"Next evolution(s)":[{"Number":"082","Name":"Magneton"}]},{"Number":"082","Name":"Magneton","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"60.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"081","Name":"Magnemite"}]},{"Number":"083","Name":"Farfetch'd","Classification":"Wild Duck Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"15.0 kg","Height":"0.8 m"},{"Number":"084","Name":"Doduo","Classification":"Twin Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"39.2 kg","Height":"1.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Doduo candies"},"Next evolution(s)":[{"Number":"085","Name":"Dodrio"}]},{"Number":"085","Name":"Dodrio","Classification":"Triple Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Feint Attack","Steel Wing"],"Weight":"85.2 kg","Height":"1.8 m","Previous evolution(s)":[{"Number":"084","Name":"Doduo"}]},{"Number":"086","Name":"Seel","Classification":"Sea Lion Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Water Gun"],"Weight":"90.0 kg","Height":"1.1 m","Next Evolution Requirements":{"Amount":50,"Name":"Seel candies"},"Next evolution(s)":[{"Number":"087","Name":"Dewgong"}]},{"Number":"087","Name":"Dewgong","Classification":"Sea Lion Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"120.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"086","Name":"Seel"}]},{"Number":"088","Name":"Grimer","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Mud Slap"],"Weight":"30.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Grimer candies"},"Next evolution(s)":[{"Number":"089","Name":"Muk"}]},{"Number":"089","Name":"Muk","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Poison Jab",""],"Weight":"30.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"088","Name":"Grimer"}]},{"Number":"090","Name":"Shellder","Classification":"Bivalve Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Tackle"],"Weight":"4.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Shellder candies"},"Next evolution(s)":[{"Number":"091","Name":"Cloyster"}]},{"Number":"091","Name":"Cloyster","Classification":"Bivalve Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"132.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"090","Name":"Shellder"}]},{"Number":"092","Name":"Gastly","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Sucker Punch"],"Weight":"0.1 kg","Height":"1.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"093","Name":"Haunter"},{"Number":"094","Name":"Gengar"}]},{"Number":"093","Name":"Haunter","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Shadow Claw"],"Weight":"0.1 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"}],"Next Evolution Requirements":{"Amount":100,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"094","Name":"Gengar"}]},{"Number":"094","Name":"Gengar","Classification":"Shadow Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Shadow Claw","Sucker Punch"],"Weight":"40.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"},{"Number":"093","Name":"Haunter"}]},{"Number":"095","Name":"Onix","Classification":"Rock Snake Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"210.0 kg","Height":"8.8 m"},{"Number":"096","Name":"Drowzee","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Pound"],"Weight":"32.4 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Drowzee candies"},"Next evolution(s)":[{"Number":"097","Name":"Hypno"}]},{"Number":"097","Name":"Hypno","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"75.6 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"096","Name":"Drowzee"}]},{"Number":"098","Name":"Krabby","Classification":"River Crab Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Krabby candies"},"Next evolution(s)":[{"Number":"099","Name":"Kingler"}]},{"Number":"099","Name":"Kingler","Classification":"Pincer Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"098","Name":"Krabby"}]},{"Number":"100","Name":"Voltorb","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark","Tackle"],"Weight":"10.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Voltorb candies"},"Next evolution(s)":[{"Number":"101","Name":"Electrode"}]},{"Number":"101","Name":"Electrode","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark",""],"Weight":"66.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"100","Name":"Voltorb"}]},{"Number":"102","Name":"Exeggcute","Classification":"Egg Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion",""],"Weight":"2.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Exeggcute candies"},"Next evolution(s)":[{"Number":"103","Name":"Exeggutor"}]},{"Number":"103","Name":"Exeggutor","Classification":"Coconut Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"120.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"102","Name":"Exeggcute"}]},{"Number":"104","Name":"Cubone","Classification":"Lonely Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Cubone candies"},"Next evolution(s)":[{"Number":"105","Name":"Marowak"}]},{"Number":"105","Name":"Marowak","Classification":"Bone Keeper Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"45.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"104","Name":"Cubone"}]},{"Number":"106","Name":"Hitmonlee","Classification":"Kicking Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Low Kick","Rock Smash"],"Weight":"49.8 kg","Height":"1.5 m","Next evolution(s)":[{"Number":"107","Name":"Hitmonchan"}]},{"Number":"107","Name":"Hitmonchan","Classification":"Punching Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Rock Smash"],"Weight":"50.2 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"106","Name":"Hitmonlee"}]},{"Number":"108","Name":"Lickitung","Classification":"Licking Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"65.5 kg","Height":"1.2 m"},{"Number":"109","Name":"Koffing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"1.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Koffing candies"},"Next evolution(s)":[{"Number":"110","Name":"Weezing"}]},{"Number":"110","Name":"Weezing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"9.5 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"109","Name":"Koffing"}]},{"Number":"111","Name":"Rhyhorn","Classification":"Spikes Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"115.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Rhyhorn candies"},"Next evolution(s)":[{"Number":"112","Name":"Rhydon"}]},{"Number":"112","Name":"Rhydon","Classification":"Drill Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"120.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"111","Name":"Rhyhorn"}]},{"Number":"113","Name":"Chansey","Classification":"Egg Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"34.6 kg","Height":"1.1 m"},{"Number":"114","Name":"Tangela","Classification":"Vine Pokemon","Type I":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug"],"Fast Attack(s)":["Vine Whip",""],"Weight":"35.0 kg","Height":"1.0 m"},{"Number":"115","Name":"Kangaskhan","Classification":"Parent Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Low Kick",""],"Weight":"80.0 kg","Height":"2.2 m"},{"Number":"116","Name":"Horsea","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Water Gun"],"Weight":"8.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Horsea candies"},"Next evolution(s)":[{"Number":"117","Name":"Seadra"}]},{"Number":"117","Name":"Seadra","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Dragon Breath","Water Gun"],"Weight":"25.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"116","Name":"Horsea"}]},{"Number":"118","Name":"Goldeen","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Mud Shot"],"Weight":"15.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Goldeen candies"},"Next evolution(s)":[{"Number":"119","Name":"Seaking"}]},{"Number":"119","Name":"Seaking","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Poison Jab"],"Weight":"39.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"118","Name":"Goldeen"}]},{"Number":"120","Name":"Staryu","Classification":"Starshape Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"34.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Staryu candies"},"Next evolution(s)":[{"Number":"120","Name":"Staryu"}]},{"Number":"121","Name":"Starmie","Classification":"Mysterious Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"80.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"121","Name":"Starmie"}]},{"Number":"122","Name":"Mr. Mime","Classification":"Barrier Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"54.5 kg","Height":"1.3 m"},{"Number":"123","Name":"Scyther","Classification":"Mantis Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Steel Wing"],"Weight":"56.0 kg","Height":"1.5 m"},{"Number":"124","Name":"Jynx","Classification":"Humanshape Pokemon","Type I":["Ice"],"Type II":["Psychic"],"Weaknesses":["Fire","Bug","Rock","Ghost","Dark","Steel"],"Fast Attack(s)":["Frost Breath","Pound"],"Weight":"40.6 kg","Height":"1.4 m"},{"Number":"125","Name":"Electabuzz","Classification":"Electric Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Low Kick","Thunder Shock"],"Weight":"30.0 kg","Height":"1.1 m"},{"Number":"126","Name":"Magmar","Classification":"Spitfire Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Karate Chop"],"Weight":"44.5 kg","Height":"1.3 m"},{"Number":"127","Name":"Pinsir","Classification":"Stagbeetle Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Rock Smash"],"Weight":"55.0 kg","Height":"1.5 m"},{"Number":"128","Name":"Tauros","Classification":"Wild Bull Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Tackle","Zen Headbutt"],"Weight":"88.4 kg","Height":"1.4 m"},{"Number":"129","Name":"Magikarp","Classification":"Fish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Splash",""],"Weight":"10.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":400,"Name":"Magikarp candies"},"Next evolution(s)":[{"Number":"130","Name":"Gyarados"}]},{"Number":"130","Name":"Gyarados","Classification":"Atrocious Pokemon","Type I":["Water"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Bite","Dragon Breath"],"Weight":"235.0 kg","Height":"6.5 m","Previous evolution(s)":[{"Number":"129","Name":"Magikarp"}]},{"Number":"131","Name":"Lapras","Classification":"Transport Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"220.0 kg","Height":"2.5 m"},{"Number":"132","Name":"Ditto","Classification":"Transform Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.3 m"},{"Number":"133","Name":"Eevee","Classification":"Evolution Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"6.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Eevee candies"},"Next evolution(s)":[{"Number":"134","Name":"Vaporeon"},{"Number":"135","Name":"Jolteon"},{"Number":"136","Name":"Flareon"}]},{"Number":"134","Name":"Vaporeon","Classification":"Bubble Jet Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun",""],"Weight":"29.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"135","Name":"Jolteon","Classification":"Lightning Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock",""],"Weight":"24.5 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"136","Name":"Flareon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"25.0 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"137","Name":"Porygon","Classification":"Virtual Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"36.5 kg","Height":"0.8 m"},{"Number":"138","Name":"Omanyte","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Water Gun",""],"Weight":"7.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Omanyte candies"},"Next evolution(s)":[{"Number":"139","Name":"Omastar"}]},{"Number":"139","Name":"Omastar","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Rock Throw","Water Gun"],"Weight":"35.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"138","Name":"Omanyte"}]},{"Number":"140","Name":"Kabuto","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"11.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Kabuto candies"},"Next evolution(s)":[{"Number":"141","Name":"Kabutops"}]},{"Number":"141","Name":"Kabutops","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Fury Cutter","Mud Shot"],"Weight":"40.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"140","Name":"Kabuto"}]},{"Number":"142","Name":"Aerodactyl","Classification":"Fossil Pokemon","Type I":["Rock"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Ice","Rock","Steel"],"Fast Attack(s)":["Bite","Steel Wing"],"Weight":"59.0 kg","Height":"1.8 m"},{"Number":"143","Name":"Snorlax","Classification":"Sleeping Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"460.0 kg","Height":"2.1 m"},{"Number":"144","Name":"Articuno","Classification":"Freeze Pokemon","Type I":["Ice"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Rock","Steel"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"55.4 kg","Height":"1.7 m"},{"Number":"145","Name":"Zapdos","Classification":"Electric Pokemon","Type I":["Electric"],"Type II":["Flying"],"Weaknesses":["Ice","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"52.6 kg","Height":"1.6 m"},{"Number":"146","Name":"Moltres","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"60.0 kg","Height":"2.0 m"},{"Number":"147","Name":"Dratini","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"3.3 kg","Height":"1.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Dratini candies"}},{"Number":"148","Name":"Dragonair","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"16.5 kg","Height":"4.0 m","Next Evolution Requirements":{"Amount":100,"Name":"Dratini candies"},"Next evolution(s)":[{"Number":"149","Name":"Dragonite"}]},{"Number":"149","Name":"Dragonite","Classification":"Dragon Pokemon","Type I":["Dragon"],"Type II":["Flying"],"Weaknesses":["Ice","Rock","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath","Steel Wing"],"Weight":"210.0 kg","Height":"2.2 m","Previous evolution(s)":[{"Number":"148","Name":"Dragonair"}]},{"Number":"150","Name":"Mewtwo","Classification":"Genetic Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"122.0 kg","Height":"2.0 m"},{"Number":"151","Name":"Mew","Classification":"New Species Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.4 m"}] \ No newline at end of file +[ + { + "Number": "001", + "Name": "Bulbasaur", + "Classification": "Seed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Tackle", + "Vine Whip" + ], + "Weight": "6.9 kg", + "Height": "0.7 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 1, + "Name": "Bulbasaur candies" + }, + "Next evolution(s)": [ + { + "Number": "002", + "Name": "Ivysaur" + }, + { + "Number": "003", + "Name": "Venusaur" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Seed Bomb", + "Sludge Bomb" + ], + "BaseAttack": 126, + "BaseDefense": 90, + "BaseStamina": 126, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "002", + "Name": "Ivysaur", + "Classification": "Seed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Razor Leaf", + "Vine Whip" + ], + "Weight": "13.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "001", + "Name": "Bulbasaur" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 1, + "Name": "Bulbasaur candies" + }, + "Next evolution(s)": [ + { + "Number": "003", + "Name": "Venusaur" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 156, + "BaseDefense": 120, + "BaseStamina": 158, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "003", + "Name": "Venusaur", + "Classification": "Seed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Razor Leaf", + "Vine Whip" + ], + "Weight": "100.0 kg", + "Height": "2.0 m", + "Previous evolution(s)": [ + { + "Number": "001", + "Name": "Bulbasaur" + }, + { + "Number": "002", + "Name": "Ivysaur" + } + ], + "Special Attack(s)": [ + "Petal Blizzard", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 198, + "BaseDefense": 160, + "BaseStamina": 200, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "004", + "Name": "Charmander", + "Classification": "Lizard Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Scratch" + ], + "Weight": "8.5 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 4, + "Name": "Charmander candies" + }, + "Next evolution(s)": [ + { + "Number": "005", + "Name": "Charmeleon" + }, + { + "Number": "006", + "Name": "Charizard" + } + ], + "Special Attack(s)": [ + "Flame Burst", + "Flame Charge", + "Flamethrower" + ], + "BaseAttack": 128, + "BaseDefense": 78, + "BaseStamina": 108, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "005", + "Name": "Charmeleon", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Scratch" + ], + "Weight": "19.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "004", + "Name": "Charmander" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 4, + "Name": "Charmander candies" + }, + "Next evolution(s)": [ + { + "Number": "006", + "Name": "Charizard" + } + ], + "Special Attack(s)": [ + "Fire Punch", + "Flame Burst", + "Flamethrower" + ], + "BaseAttack": 160, + "BaseDefense": 116, + "BaseStamina": 140, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "006", + "Name": "Charizard", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Water", + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Wing Attack" + ], + "Weight": "90.5 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "004", + "Name": "Charmander" + }, + { + "Number": "005", + "Name": "Charmeleon" + } + ], + "Special Attack(s)": [ + "Dragon Claw", + "Fire Blast", + "Flamethrower" + ], + "BaseAttack": 212, + "BaseDefense": 156, + "BaseStamina": 182, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "007", + "Name": "Squirtle", + "Classification": "Tiny Turtle Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Tackle" + ], + "Weight": "9.0 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 7, + "Name": "Squirtle candies" + }, + "Next evolution(s)": [ + { + "Number": "008", + "Name": "Wartortle" + }, + { + "Number": "009", + "Name": "Blastoise" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Aqua Tail", + "Water Pulse" + ], + "BaseAttack": 112, + "BaseDefense": 88, + "BaseStamina": 142, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "008", + "Name": "Wartortle", + "Classification": "Turtle Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bite", + "Water Gun" + ], + "Weight": "22.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "007", + "Name": "Squirtle" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 7, + "Name": "Squirtle candies" + }, + "Next evolution(s)": [ + { + "Number": "009", + "Name": "Blastoise" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Hydro Pump", + "Ice Beam" + ], + "BaseAttack": 144, + "BaseDefense": 118, + "BaseStamina": 176, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "009", + "Name": "Blastoise", + "Classification": "Shellfish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bite", + "Water Gun" + ], + "Weight": "85.5 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "007", + "Name": "Squirtle" + }, + { + "Number": "008", + "Name": "Wartortle" + } + ], + "Special Attack(s)": [ + "Flash Cannon", + "Hydro Pump", + "Ice Beam" + ], + "BaseAttack": 186, + "BaseDefense": 158, + "BaseStamina": 222, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "010", + "Name": "Caterpie", + "Classification": "Worm Pokemon", + "Type I": [ + "Bug" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Tackle" + ], + "Weight": "2.9 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 12, + "Family": 10, + "Name": "Caterpie candies" + }, + "Next evolution(s)": [ + { + "Number": "011", + "Name": "Metapod" + }, + { + "Number": "012", + "Name": "Butterfree" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 62, + "BaseDefense": 90, + "BaseStamina": 66, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "011", + "Name": "Metapod", + "Classification": "Cocoon Pokemon", + "Type I": [ + "Bug" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Tackle" + ], + "Weight": "9.9 kg", + "Height": "0.7 m", + "Previous evolution(s)": [ + { + "Number": "010", + "Name": "Caterpie" + } + ], + "Next Evolution Requirements": { + "Amount": 50, + "Family": 10, + "Name": "Caterpie candies" + }, + "Next evolution(s)": [ + { + "Number": "012", + "Name": "Butterfree" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 56, + "BaseDefense": 100, + "BaseStamina": 86, + "CaptureRate": 0.2, + "FleeRate": 0.09 + }, + { + "Number": "012", + "Name": "Butterfree", + "Classification": "Butterfly Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Fire", + "Electric", + "Ice", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Confusion" + ], + "Weight": "32.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "010", + "Name": "Caterpie" + }, + { + "Number": "011", + "Name": "Metapod" + } + ], + "Special Attack(s)": [ + "Bug Buzz", + "Psychic", + "Signal Beam" + ], + "BaseAttack": 144, + "BaseDefense": 120, + "BaseStamina": 144, + "CaptureRate": 0.1, + "FleeRate": 0.06 + }, + { + "Number": "013", + "Name": "Weedle", + "Classification": "Hairy Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Poison Sting" + ], + "Weight": "3.2 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 12, + "Family": 13, + "Name": "Weedle candies" + }, + "Next evolution(s)": [ + { + "Number": "014", + "Name": "Kakuna" + }, + { + "Number": "015", + "Name": "Beedrill" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 68, + "BaseDefense": 80, + "BaseStamina": 64, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "014", + "Name": "Kakuna", + "Classification": "Cocoon Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Poison Sting" + ], + "Weight": "10.0 kg", + "Height": "0.6 m", + "Previous evolution(s)": [ + { + "Number": "013", + "Name": "Weedle" + } + ], + "Next Evolution Requirements": { + "Amount": 50, + "Family": 13, + "Name": "Weedle candies" + }, + "Next evolution(s)": [ + { + "Number": "015", + "Name": "Beedrill" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 62, + "BaseDefense": 90, + "BaseStamina": 82, + "CaptureRate": 0.2, + "FleeRate": 0.09 + }, + { + "Number": "015", + "Name": "Beedrill", + "Classification": "Poison Bee Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Poison Jab" + ], + "Weight": "29.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "013", + "Name": "Weedle" + }, + { + "Number": "014", + "Name": "Kakuna" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Sludge Bomb", + "X Scissor" + ], + "BaseAttack": 144, + "BaseDefense": 130, + "BaseStamina": 130, + "CaptureRate": 0.1, + "FleeRate": 0.06 + }, + { + "Number": "016", + "Name": "Pidgey", + "Classification": "Tiny Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Twister" + ], + "Weight": "1.8 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 12, + "Family": 16, + "Name": "Pidgey candies" + }, + "Next evolution(s)": [ + { + "Number": "017", + "Name": "Pidgeotto" + }, + { + "Number": "018", + "Name": "Pidgeot" + } + ], + "BaseAttack": 94, + "BaseDefense": 80, + "BaseStamina": 90, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "017", + "Name": "Pidgeotto", + "Classification": "Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Steel Wing", + "Wing Attack" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Twister" + ], + "Weight": "30.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "016", + "Name": "Pidgey" + } + ], + "Next Evolution Requirements": { + "Amount": 50, + "Family": 16, + "Name": "Pidgey candies" + }, + "Next evolution(s)": [ + { + "Number": "018", + "Name": "Pidgeot" + } + ], + "BaseAttack": 126, + "BaseDefense": 126, + "BaseStamina": 122, + "CaptureRate": 0.2, + "FleeRate": 0.09 + }, + { + "Number": "018", + "Name": "Pidgeot", + "Classification": "Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Steel Wing", + "Wing Attack" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Hurricane" + ], + "Weight": "39.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "016", + "Name": "Pidgey" + }, + { + "Number": "017", + "Name": "Pidgeotto" + } + ], + "BaseAttack": 170, + "BaseDefense": 166, + "BaseStamina": 166, + "CaptureRate": 0.1, + "FleeRate": 0.06 + }, + { + "Number": "019", + "Name": "Rattata", + "Classification": "Mouse Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Special Attack(s)": [ + "Body Slam", + "Dig", + "Hyper Fang" + ], + "Weight": "3.5 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 19, + "Name": "Rattata candies" + }, + "Next evolution(s)": [ + { + "Number": "020", + "Name": "Raticate" + } + ], + "BaseAttack": 92, + "BaseDefense": 60, + "BaseStamina": 86, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "020", + "Name": "Raticate", + "Classification": "Mouse Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Bite", + "Quick Attack" + ], + "Special Attack(s)": [ + "Dig", + "Hyper Beam", + "Hyper Fang" + ], + "Weight": "18.5 kg", + "Height": "0.7 m", + "Previous evolution(s)": [ + { + "Number": "019", + "Name": "Rattata" + } + ], + "BaseAttack": 146, + "BaseDefense": 110, + "BaseStamina": 150, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "021", + "Name": "Spearow", + "Classification": "Tiny Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Peck", + "Quick Attack" + ], + "Weight": "2.0 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 21, + "Name": "Spearow candies" + }, + "Next evolution(s)": [ + { + "Number": "022", + "Name": "Fearow" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Drill Peck", + "Twister" + ], + "BaseAttack": 102, + "BaseDefense": 80, + "BaseStamina": 78, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "022", + "Name": "Fearow", + "Classification": "Beak Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Peck", + "Steel Wing" + ], + "Weight": "38.0 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "021", + "Name": "Spearow" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Drill Run", + "Twister" + ], + "BaseAttack": 168, + "BaseDefense": 130, + "BaseStamina": 146, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "023", + "Name": "Ekans", + "Classification": "Snake Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Poison Sting" + ], + "Weight": "6.9 kg", + "Height": "2.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 23, + "Name": "Ekans candies" + }, + "Next evolution(s)": [ + { + "Number": "024", + "Name": "Arbok" + } + ], + "Special Attack(s)": [ + "Gunk Shot", + "Sludge Bomb", + "Wrap" + ], + "BaseAttack": 112, + "BaseDefense": 70, + "BaseStamina": 112, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "024", + "Name": "Arbok", + "Classification": "Cobra Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Bite" + ], + "Weight": "65.0 kg", + "Height": "3.5 m", + "Previous evolution(s)": [ + { + "Number": "023", + "Name": "Ekans" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Gunk Shot", + "Sludge Wave" + ], + "BaseAttack": 166, + "BaseDefense": 120, + "BaseStamina": 166, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "025", + "Name": "Pikachu", + "Classification": "Mouse Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Thunder Shock" + ], + "Weight": "6.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 25, + "Name": "Pikachu candies" + }, + "Next evolution(s)": [ + { + "Number": "026", + "Name": "Raichu" + } + ], + "Special Attack(s)": [ + "Discharge", + "Thunder", + "Thunderbolt" + ], + "BaseAttack": 124, + "BaseDefense": 70, + "BaseStamina": 108, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "026", + "Name": "Raichu", + "Classification": "Mouse Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Thunder Shock" + ], + "Weight": "30.0 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "025", + "Name": "Pikachu" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Thunder", + "Thunder Punch" + ], + "BaseAttack": 200, + "BaseDefense": 120, + "BaseStamina": 154, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "027", + "Name": "Sandshrew", + "Classification": "Mouse Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Scratch" + ], + "Weight": "12.0 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 27, + "Name": "Sandshrew candies" + }, + "Next evolution(s)": [ + { + "Number": "028", + "Name": "Sandslash" + } + ], + "Special Attack(s)": [ + "Dig", + "Rock Slide", + "Rock Tomb" + ], + "BaseAttack": 90, + "BaseDefense": 100, + "BaseStamina": 114, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "028", + "Name": "Sandslash", + "Classification": "Mouse Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Metal Claw", + "Mud Shot" + ], + "Weight": "29.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "027", + "Name": "Sandshrew" + } + ], + "Special Attack(s)": [ + "Bulldoze", + "Earthquake", + "Rock Tomb" + ], + "BaseAttack": 150, + "BaseDefense": 150, + "BaseStamina": 172, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "029", + "Name": "Nidoran F", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bite", + "Poison Sting" + ], + "Weight": "7.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 29, + "Name": "Nidoran F candies" + }, + "Next evolution(s)": [ + { + "Number": "030", + "Name": "Nidorina" + }, + { + "Number": "031", + "Name": "Nidoqueen" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Poison Fang", + "Sludge Bomb" + ], + "BaseAttack": 100, + "BaseDefense": 110, + "BaseStamina": 104, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "030", + "Name": "Nidorina", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bite", + "Poison Sting" + ], + "Weight": "20.0 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "029", + "Name": "Nidoran F" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 29, + "Name": "Nidoran F candies" + }, + "Next evolution(s)": [ + { + "Number": "031", + "Name": "Nidoqueen" + } + ], + "Special Attack(s)": [ + "Dig", + "Poison Fang", + "Sludge Bomb" + ], + "BaseAttack": 132, + "BaseDefense": 140, + "BaseStamina": 136, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "031", + "Name": "Nidoqueen", + "Classification": "Drill Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Ice", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bite", + "Poison Jab" + ], + "Weight": "60.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "029", + "Name": "Nidoran F" + }, + { + "Number": "030", + "Name": "Nidorina" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Sludge Wave", + "Stone Edge" + ], + "BaseAttack": 184, + "BaseDefense": 180, + "BaseStamina": 190, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "032", + "Name": "Nidoran M", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Peck", + "Poison Sting" + ], + "Weight": "9.0 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 32, + "Name": "Nidoran M candies" + }, + "Next evolution(s)": [ + { + "Number": "033", + "Name": "Nidorino" + }, + { + "Number": "034", + "Name": "Nidoking" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Horn Attack", + "Sludge Bomb" + ], + "BaseAttack": 110, + "BaseDefense": 92, + "BaseStamina": 94, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "033", + "Name": "Nidorino", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Poison Jab", + "Poison Sting" + ], + "Weight": "19.5 kg", + "Height": "0.9 m", + "Previous evolution(s)": [ + { + "Number": "032", + "Name": "Nidoran M" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 32, + "Name": "Nidoran M candies" + }, + "Next evolution(s)": [ + { + "Number": "034", + "Name": "Nidoking" + } + ], + "Special Attack(s)": [ + "Dig", + "Horn Attack", + "Sludge Bomb" + ], + "BaseAttack": 142, + "BaseDefense": 122, + "BaseStamina": 128, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "034", + "Name": "Nidoking", + "Classification": "Drill Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Ice", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Poison Jab" + ], + "Weight": "62.0 kg", + "Height": "1.4 m", + "Previous evolution(s)": [ + { + "Number": "032", + "Name": "Nidoran M" + }, + { + "Number": "033", + "Name": "Nidorino" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Megahorn", + "Sludge Wave" + ], + "BaseAttack": 204, + "BaseDefense": 162, + "BaseStamina": 170, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "035", + "Name": "Clefairy", + "Classification": "Fairy Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound", + "Zen Headbutt" + ], + "Weight": "7.5 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 35, + "Name": "Clefairy candies" + }, + "Next evolution(s)": [ + { + "Number": "036", + "Name": "Clefable" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Disarming Voice", + "Moonblast" + ], + "BaseAttack": 116, + "BaseDefense": 140, + "BaseStamina": 124, + "CaptureRate": 0.24, + "FleeRate": 0.1 + }, + { + "Number": "036", + "Name": "Clefable", + "Classification": "Fairy Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound", + "Zen Headbutt" + ], + "Weight": "40.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "035", + "Name": "Clefairy" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Moonblast", + "Psychic" + ], + "BaseAttack": 178, + "BaseDefense": 190, + "BaseStamina": 178, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "037", + "Name": "Vulpix", + "Classification": "Fox Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Quick Attack" + ], + "Weight": "9.9 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 37, + "Name": "Vulpix candies" + }, + "Next evolution(s)": [ + { + "Number": "038", + "Name": "Ninetales" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Flame Charge", + "Flamethrower" + ], + "BaseAttack": 106, + "BaseDefense": 76, + "BaseStamina": 118, + "CaptureRate": 0.24, + "FleeRate": 0.1 + }, + { + "Number": "038", + "Name": "Ninetales", + "Classification": "Fox Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Feint Attack" + ], + "Weight": "19.9 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "037", + "Name": "Vulpix" + } + ], + "Special Attack(s)": [ + "Fire Blast", + "Flamethrower", + "Heat Wave" + ], + "BaseAttack": 176, + "BaseDefense": 146, + "BaseStamina": 194, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "039", + "Name": "Jigglypuff", + "Classification": "Balloon Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Pound" + ], + "Weight": "5.5 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 39, + "Name": "Jigglypuff candies" + }, + "Next evolution(s)": [ + { + "Number": "039", + "Name": "Jigglypuff" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Disarming Voice", + "Play Rough" + ], + "BaseAttack": 98, + "BaseDefense": 230, + "BaseStamina": 54, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "040", + "Name": "Wigglytuff", + "Classification": "Balloon Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Pound" + ], + "Weight": "12.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "040", + "Name": "Wigglytuff" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Hyper Beam", + "Play Rough" + ], + "BaseAttack": 168, + "BaseDefense": 280, + "BaseStamina": 108, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "041", + "Name": "Zubat", + "Classification": "Bat Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Ice", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Quick Attack" + ], + "Weight": "7.5 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 41, + "Name": "Zubat candies" + }, + "Next evolution(s)": [ + { + "Number": "042", + "Name": "Golbat" + } + ], + "Special Attack(s)": [ + "Air Cutter", + "Poison Fang", + "Sludge Bomb" + ], + "BaseAttack": 88, + "BaseDefense": 80, + "BaseStamina": 90, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "042", + "Name": "Golbat", + "Classification": "Bat Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Ice", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Wing Attack" + ], + "Weight": "55.0 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "041", + "Name": "Zubat" + } + ], + "Special Attack(s)": [ + "Air Cutter", + "Ominous Wind", + "Poison Fang" + ], + "BaseAttack": 164, + "BaseDefense": 150, + "BaseStamina": 164, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "043", + "Name": "Oddish", + "Classification": "Weed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "5.4 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 43, + "Name": "Oddish candies" + }, + "Next evolution(s)": [ + { + "Number": "044", + "Name": "Gloom" + }, + { + "Number": "045", + "Name": "Vileplume" + } + ], + "Special Attack(s)": [ + "Moonblast", + "Seed Bomb", + "Sludge Bomb" + ], + "BaseAttack": 134, + "BaseDefense": 90, + "BaseStamina": 130, + "CaptureRate": 0.48, + "FleeRate": 0.15 + }, + { + "Number": "044", + "Name": "Gloom", + "Classification": "Weed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "8.6 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "043", + "Name": "Oddish" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 43, + "Name": "Oddish candies" + }, + "Next evolution(s)": [ + { + "Number": "045", + "Name": "Vileplume" + } + ], + "Special Attack(s)": [ + "Moonblast", + "Petal Blizzard", + "Sludge Bomb" + ], + "BaseAttack": 162, + "BaseDefense": 120, + "BaseStamina": 158, + "CaptureRate": 0.24, + "FleeRate": 0.07 + }, + { + "Number": "045", + "Name": "Vileplume", + "Classification": "Flower Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "18.6 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "043", + "Name": "Oddish" + }, + { + "Number": "044", + "Name": "Gloom" + } + ], + "Special Attack(s)": [ + "Moonblast", + "Petal Blizzard", + "Solar Beam" + ], + "BaseAttack": 202, + "BaseDefense": 150, + "BaseStamina": 190, + "CaptureRate": 0.12, + "FleeRate": 0.05 + }, + { + "Number": "046", + "Name": "Paras", + "Classification": "Mushroom Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Grass" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Scratch" + ], + "Weight": "5.4 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 46, + "Name": "Paras candies" + }, + "Next evolution(s)": [ + { + "Number": "047", + "Name": "Parasect" + } + ], + "Special Attack(s)": [ + "Cross Poison", + "Seed Bomb", + "X Scissor" + ], + "BaseAttack": 122, + "BaseDefense": 70, + "BaseStamina": 120, + "CaptureRate": 0.32, + "FleeRate": 0.15 + }, + { + "Number": "047", + "Name": "Parasect", + "Classification": "Mushroom Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Grass" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Fury Cutter" + ], + "Weight": "29.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "046", + "Name": "Paras" + } + ], + "Special Attack(s)": [ + "Cross Poison", + "Solar Beam", + "X Scissor" + ], + "BaseAttack": 162, + "BaseDefense": 120, + "BaseStamina": 170, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "048", + "Name": "Venonat", + "Classification": "Insect Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Confusion" + ], + "Weight": "30.0 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 48, + "Name": "Venonat candies" + }, + "Next evolution(s)": [ + { + "Number": "049", + "Name": "Venomoth" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Psybeam", + "Poison Fang", + "Shadow Ball" + ], + "BaseAttack": 108, + "BaseDefense": 120, + "BaseStamina": 118, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "049", + "Name": "Venomoth", + "Classification": "Poison Moth Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Confusion" + ], + "Weight": "12.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "048", + "Name": "Venonat" + } + ], + "Special Attack(s)": [ + "Bug Buzz", + "Poison Fang", + "Psychic" + ], + "BaseAttack": 172, + "BaseDefense": 140, + "BaseStamina": 154, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "050", + "Name": "Diglett", + "Classification": "Mole Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Scratch" + ], + "Weight": "0.8 kg", + "Height": "0.2 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 50, + "Name": "Diglett candies" + }, + "Next evolution(s)": [ + { + "Number": "051", + "Name": "Dugtrio" + } + ], + "Special Attack(s)": [ + "Dig", + "Mud Bomb", + "Rock Tomb" + ], + "BaseAttack": 108, + "BaseDefense": 20, + "BaseStamina": 86, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "051", + "Name": "Dugtrio", + "Classification": "Mole Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Sucker Punch" + ], + "Weight": "33.3 kg", + "Height": "0.7 m", + "Previous evolution(s)": [ + { + "Number": "050", + "Name": "Diglett" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Mud Bomb", + "Stone Edge" + ], + "BaseAttack": 148, + "BaseDefense": 70, + "BaseStamina": 140, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "052", + "Name": "Meowth", + "Classification": "Scratch Cat Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Bite", + "Scratch" + ], + "Weight": "4.2 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 52, + "Name": "Meowth candies" + }, + "Next evolution(s)": [ + { + "Number": "053", + "Name": "Persian" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Dark Pulse", + "Night Slash" + ], + "BaseAttack": 104, + "BaseDefense": 80, + "BaseStamina": 94, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "053", + "Name": "Persian", + "Classification": "Classy Cat Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Scratch" + ], + "Weight": "32.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "052", + "Name": "Meowth" + } + ], + "Special Attack(s)": [ + "Night Slash", + "Play Rough", + "Power Gem" + ], + "BaseAttack": 156, + "BaseDefense": 130, + "BaseStamina": 146, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "054", + "Name": "Psyduck", + "Classification": "Duck Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Water Gun", + "Zen Headbutt" + ], + "Weight": "19.6 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 54, + "Name": "Psyduck candies" + }, + "Next evolution(s)": [ + { + "Number": "055", + "Name": "Golduck" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Cross Chop", + "Psybeam" + ], + "BaseAttack": 132, + "BaseDefense": 100, + "BaseStamina": 112, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "055", + "Name": "Golduck", + "Classification": "Duck Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Confusion", + "Water Gun" + ], + "Weight": "76.6 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "054", + "Name": "Psyduck" + } + ], + "Special Attack(s)": [ + "Hydro Pump", + "Ice Beam", + "Psychic" + ], + "BaseAttack": 194, + "BaseDefense": 160, + "BaseStamina": 176, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "056", + "Name": "Mankey", + "Classification": "Pig Monkey Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Scratch" + ], + "Weight": "28.0 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 56, + "Name": "Mankey candies" + }, + "Next evolution(s)": [ + { + "Number": "057", + "Name": "Primeape" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Cross Chop", + "Low Sweep" + ], + "BaseAttack": 122, + "BaseDefense": 80, + "BaseStamina": 96, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "057", + "Name": "Primeape", + "Classification": "Pig Monkey Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Low Kick" + ], + "Weight": "32.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "056", + "Name": "Mankey" + } + ], + "Special Attack(s)": [ + "Cross Chop", + "Low Sweep", + "Night Slash" + ], + "BaseAttack": 178, + "BaseDefense": 130, + "BaseStamina": 150, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "058", + "Name": "Growlithe", + "Classification": "Puppy Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Ember" + ], + "Weight": "19.0 kg", + "Height": "0.7 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 58, + "Name": "Growlithe candies" + }, + "Next evolution(s)": [ + { + "Number": "059", + "Name": "Arcanine" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Flame Wheel", + "Flamethrower" + ], + "BaseAttack": 156, + "BaseDefense": 110, + "BaseStamina": 110, + "CaptureRate": 0.24, + "FleeRate": 0.1 + }, + { + "Number": "059", + "Name": "Arcanine", + "Classification": "Legendary Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Fire Fang" + ], + "Weight": "155.0 kg", + "Height": "1.9 m", + "Previous evolution(s)": [ + { + "Number": "058", + "Name": "Growlithe" + } + ], + "Special Attack(s)": [ + "Bulldoze", + "Fire Blast", + "Flamethrower" + ], + "BaseAttack": 230, + "BaseDefense": 180, + "BaseStamina": 180, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "060", + "Name": "Poliwag", + "Classification": "Tadpole Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "12.4 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 60, + "Name": "Poliwag candies" + }, + "Next evolution(s)": [ + { + "Number": "061", + "Name": "Poliwhirl" + }, + { + "Number": "062", + "Name": "Poliwrath" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Bubble Beam", + "Mud Bomb" + ], + "BaseAttack": 108, + "BaseDefense": 80, + "BaseStamina": 98, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "061", + "Name": "Poliwhirl", + "Classification": "Tadpole Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "20.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "060", + "Name": "Poliwag" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 60, + "Name": "Poliwag candies" + }, + "Next evolution(s)": [ + { + "Number": "062", + "Name": "Poliwrath" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Mud Bomb", + "Scald" + ], + "BaseAttack": 132, + "BaseDefense": 130, + "BaseStamina": 132, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "062", + "Name": "Poliwrath", + "Classification": "Tadpole Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Fighting" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "54.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "060", + "Name": "Poliwag" + }, + { + "Number": "061", + "Name": "Poliwhirl" + } + ], + "Special Attack(s)": [ + "Hydro Pump", + "Ice Punch", + "Submission" + ], + "BaseAttack": 180, + "BaseDefense": 180, + "BaseStamina": 202, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "063", + "Name": "Abra", + "Classification": "Psi Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Zen Headbutt" + ], + "Weight": "19.5 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 63, + "Name": "Abra candies" + }, + "Next evolution(s)": [ + { + "Number": "064", + "Name": "Kadabra" + }, + { + "Number": "065", + "Name": "Alakazam" + } + ], + "Special Attack(s)": [ + "Psyshock", + "Shadow Ball", + "Signal Beam" + ], + "BaseAttack": 110, + "BaseDefense": 50, + "BaseStamina": 76, + "CaptureRate": 0.4, + "FleeRate": 0.99 + }, + { + "Number": "064", + "Name": "Kadabra", + "Classification": "Psi Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Psycho Cut" + ], + "Weight": "56.5 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "063", + "Name": "Abra" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 63, + "Name": "Abra candies" + }, + "Next evolution(s)": [ + { + "Number": "065", + "Name": "Alakazam" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Psybeam", + "Shadow Ball" + ], + "BaseAttack": 150, + "BaseDefense": 80, + "BaseStamina": 112, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "065", + "Name": "Alakazam", + "Classification": "Psi Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Psycho Cut" + ], + "Weight": "48.0 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "063", + "Name": "Abra" + }, + { + "Number": "064", + "Name": "Kadabra" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Psychic", + "Shadow Ball" + ], + "BaseAttack": 186, + "BaseDefense": 110, + "BaseStamina": 152, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "066", + "Name": "Machop", + "Classification": "Superpower Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Low Kick" + ], + "Weight": "19.5 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 66, + "Name": "Machop candies" + }, + "Next evolution(s)": [ + { + "Number": "067", + "Name": "Machoke" + }, + { + "Number": "068", + "Name": "Machamp" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Cross Chop", + "Low Sweep" + ], + "BaseAttack": 118, + "BaseDefense": 140, + "BaseStamina": 96, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "067", + "Name": "Machoke", + "Classification": "Superpower Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Low Kick" + ], + "Weight": "70.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "066", + "Name": "Machop" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 66, + "Name": "Machop candies" + }, + "Next evolution(s)": [ + { + "Number": "068", + "Name": "Machamp" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Cross Chop", + "Submission" + ], + "BaseAttack": 154, + "BaseDefense": 160, + "BaseStamina": 144, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "068", + "Name": "Machamp", + "Classification": "Superpower Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Bullet Punch", + "Karate Chop" + ], + "Weight": "130.0 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "066", + "Name": "Machop" + }, + { + "Number": "067", + "Name": "Machoke" + } + ], + "Special Attack(s)": [ + "Cross Chop", + "Stone Edge", + "Submission" + ], + "BaseAttack": 198, + "BaseDefense": 180, + "BaseStamina": 180, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "069", + "Name": "Bellsprout", + "Classification": "Flower Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Vine Whip" + ], + "Weight": "4.0 kg", + "Height": "0.7 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 69, + "Name": "Bellsprout candies" + }, + "Next evolution(s)": [ + { + "Number": "070", + "Name": "Weepinbell" + }, + { + "Number": "071", + "Name": "Victreebel" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Sludge Bomb", + "Wrap" + ], + "BaseAttack": 158, + "BaseDefense": 100, + "BaseStamina": 78, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "070", + "Name": "Weepinbell", + "Classification": "Flycatcher Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "6.4 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "069", + "Name": "Bellsprout" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 69, + "Name": "Bellsprout candies" + }, + "Next evolution(s)": [ + { + "Number": "071", + "Name": "Victreebel" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Seed Bomb", + "Sludge Bomb" + ], + "BaseAttack": 190, + "BaseDefense": 130, + "BaseStamina": 110, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "071", + "Name": "Victreebel", + "Classification": "Flycatcher Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "15.5 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "069", + "Name": "Bellsprout" + }, + { + "Number": "070", + "Name": "Weepinbell" + } + ], + "Special Attack(s)": [ + "Leaf Blade", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 222, + "BaseDefense": 160, + "BaseStamina": 152, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "072", + "Name": "Tentacool", + "Classification": "Jellyfish Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Electric", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bubble", + "Poison Sting" + ], + "Weight": "45.5 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 72, + "Name": "Tentacool candies" + }, + "Next evolution(s)": [ + { + "Number": "073", + "Name": "Tentacruel" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Water Pulse", + "Wrap" + ], + "BaseAttack": 106, + "BaseDefense": 80, + "BaseStamina": 136, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "073", + "Name": "Tentacruel", + "Classification": "Jellyfish Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Electric", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Poison Jab" + ], + "Weight": "55.0 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "072", + "Name": "Tentacool" + } + ], + "Special Attack(s)": [ + "Blizzard", + "Hydro Pump", + "Sludge Wave" + ], + "BaseAttack": 170, + "BaseDefense": 160, + "BaseStamina": 196, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "074", + "Name": "Geodude", + "Classification": "Rock Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Rock Throw", + "Tackle" + ], + "Weight": "20.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 74, + "Name": "Geodude candies" + }, + "Next evolution(s)": [ + { + "Number": "075", + "Name": "Graveler" + }, + { + "Number": "076", + "Name": "Golem" + } + ], + "Special Attack(s)": [ + "Dig", + "Rock Slide", + "Rock Tomb" + ], + "BaseAttack": 106, + "BaseDefense": 80, + "BaseStamina": 118, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "075", + "Name": "Graveler", + "Classification": "Rock Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Rock Throw" + ], + "Weight": "105.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "074", + "Name": "Geodude" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 74, + "Name": "Geodude candies" + }, + "Next evolution(s)": [ + { + "Number": "076", + "Name": "Golem" + } + ], + "Special Attack(s)": [ + "Dig", + "Rock Slide", + "Stone Edge" + ], + "BaseAttack": 142, + "BaseDefense": 110, + "BaseStamina": 156, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "076", + "Name": "Golem", + "Classification": "Megaton Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Rock Throw" + ], + "Weight": "300.0 kg", + "Height": "1.4 m", + "Previous evolution(s)": [ + { + "Number": "074", + "Name": "Geodude" + }, + { + "Number": "075", + "Name": "Graveler" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Earthquake", + "Stone Edge" + ], + "BaseAttack": 176, + "BaseDefense": 160, + "BaseStamina": 198, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "077", + "Name": "Ponyta", + "Classification": "Fire Horse Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Tackle" + ], + "Weight": "30.0 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 77, + "Name": "Ponyta candies" + }, + "Next evolution(s)": [ + { + "Number": "078", + "Name": "Rapidash" + } + ], + "Special Attack(s)": [ + "Fire Blast", + "Flame Charge", + "Flame Wheel" + ], + "BaseAttack": 168, + "BaseDefense": 100, + "BaseStamina": 138, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "078", + "Name": "Rapidash", + "Classification": "Fire Horse Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Low Kick" + ], + "Weight": "95.0 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "077", + "Name": "Ponyta" + } + ], + "Special Attack(s)": [ + "Drill Run", + "Fire Blast", + "Heat Wave" + ], + "BaseAttack": 200, + "BaseDefense": 130, + "BaseStamina": 170, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "079", + "Name": "Slowpoke", + "Classification": "Dopey Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Water Gun" + ], + "Weight": "36.0 kg", + "Height": "1.2 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 79, + "Name": "Slowpoke candies" + }, + "Next evolution(s)": [ + { + "Number": "080", + "Name": "Slowbro" + } + ], + "Special Attack(s)": [ + "Psychic", + "Psyshock", + "Water Pulse" + ], + "BaseAttack": 110, + "BaseDefense": 180, + "BaseStamina": 110, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "080", + "Name": "Slowbro", + "Classification": "Hermit Crab Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Water Gun" + ], + "Weight": "78.5 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "079", + "Name": "Slowpoke" + } + ], + "Special Attack(s)": [ + "Ice Beam", + "Psychic", + "Water Pulse" + ], + "BaseAttack": 184, + "BaseDefense": 190, + "BaseStamina": 198, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "081", + "Name": "Magnemite", + "Classification": "Magnet Pokemon", + "Type I": [ + "Electric" + ], + "Type II": [ + "Steel" + ], + "Weaknesses": [ + "Fire", + "Water", + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Thunder Shock" + ], + "Weight": "6.0 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 81, + "Name": "Magnemite candies" + }, + "Next evolution(s)": [ + { + "Number": "082", + "Name": "Magneton" + } + ], + "Special Attack(s)": [ + "Discharge", + "Magnet Bomb", + "Thunderbolt" + ], + "BaseAttack": 128, + "BaseDefense": 50, + "BaseStamina": 138, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "082", + "Name": "Magneton", + "Classification": "Magnet Pokemon", + "Type I": [ + "Electric" + ], + "Type II": [ + "Steel" + ], + "Weaknesses": [ + "Fire", + "Water", + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Thunder Shock" + ], + "Weight": "60.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "081", + "Name": "Magnemite" + } + ], + "Special Attack(s)": [ + "Discharge", + "Flash Cannon", + "Magnet Bomb" + ], + "BaseAttack": 186, + "BaseDefense": 100, + "BaseStamina": 180, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "083", + "Name": "Farfetch'd", + "Classification": "Wild Duck Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Cut", + "Fury Cutter" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Leaf Blade" + ], + "Weight": "15.0 kg", + "Height": "0.8 m", + "BaseAttack": 138, + "BaseDefense": 104, + "BaseStamina": 132, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "084", + "Name": "Doduo", + "Classification": "Twin Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Peck", + "Quick Attack" + ], + "Weight": "39.2 kg", + "Height": "1.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 84, + "Name": "Doduo candies" + }, + "Next evolution(s)": [ + { + "Number": "085", + "Name": "Dodrio" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Drill Peck", + "Swift" + ], + "BaseAttack": 126, + "BaseDefense": 70, + "BaseStamina": 96, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "085", + "Name": "Dodrio", + "Classification": "Triple Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Steel Wing" + ], + "Weight": "85.2 kg", + "Height": "1.8 m", + "Previous evolution(s)": [ + { + "Number": "084", + "Name": "Doduo" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Drill Peck" + ], + "BaseAttack": 182, + "BaseDefense": 120, + "BaseStamina": 150, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "086", + "Name": "Seel", + "Classification": "Sea Lion Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Ice Shard", + "Water Gun" + ], + "Weight": "90.0 kg", + "Height": "1.1 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 86, + "Name": "Seel candies" + }, + "Next evolution(s)": [ + { + "Number": "087", + "Name": "Dewgong" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Aqua Tail", + "Icy Wind" + ], + "BaseAttack": 104, + "BaseDefense": 130, + "BaseStamina": 138, + "CaptureRate": 0.4, + "FleeRate": 0.09 + }, + { + "Number": "087", + "Name": "Dewgong", + "Classification": "Sea Lion Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Ice" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Rock" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Ice Shard" + ], + "Weight": "120.0 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "086", + "Name": "Seel" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Blizzard", + "Icy Wind" + ], + "BaseAttack": 156, + "BaseDefense": 180, + "BaseStamina": 192, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "088", + "Name": "Grimer", + "Classification": "Sludge Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Mud Slap" + ], + "Weight": "30.0 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 88, + "Name": "Grimer candies" + }, + "Next evolution(s)": [ + { + "Number": "089", + "Name": "Muk" + } + ], + "Special Attack(s)": [ + "Mud Bomb", + "Sludge", + "Sludge Bomb" + ], + "BaseAttack": 124, + "BaseDefense": 160, + "BaseStamina": 110, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "089", + "Name": "Muk", + "Classification": "Sludge Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Poison Jab" + ], + "Weight": "30.0 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "088", + "Name": "Grimer" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Gunk Shot", + "Sludge Wave" + ], + "BaseAttack": 180, + "BaseDefense": 210, + "BaseStamina": 188, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "090", + "Name": "Shellder", + "Classification": "Bivalve Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Ice Shard", + "Tackle" + ], + "Weight": "4.0 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 90, + "Name": "Shellder candies" + }, + "Next evolution(s)": [ + { + "Number": "091", + "Name": "Cloyster" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Icy Wind", + "Water Pulse" + ], + "BaseAttack": 120, + "BaseDefense": 60, + "BaseStamina": 112, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "091", + "Name": "Cloyster", + "Classification": "Bivalve Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Ice" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Rock" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Ice Shard" + ], + "Weight": "132.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "090", + "Name": "Shellder" + } + ], + "Special Attack(s)": [ + "Blizzard", + "Hydro Pump", + "Icy Wind" + ], + "BaseAttack": 196, + "BaseDefense": 100, + "BaseStamina": 196, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "092", + "Name": "Gastly", + "Classification": "Gas Pokemon", + "Type I": [ + "Ghost" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Lick", + "Sucker Punch" + ], + "Weight": "0.1 kg", + "Height": "1.3 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 92, + "Name": "Gastly candies" + }, + "Next evolution(s)": [ + { + "Number": "093", + "Name": "Haunter" + }, + { + "Number": "094", + "Name": "Gengar" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Ominous Wind", + "Sludge Bomb" + ], + "BaseAttack": 136, + "BaseDefense": 60, + "BaseStamina": 82, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "093", + "Name": "Haunter", + "Classification": "Gas Pokemon", + "Type I": [ + "Ghost" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Lick", + "Shadow Claw" + ], + "Weight": "0.1 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "092", + "Name": "Gastly" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 92, + "Name": "Gastly candies" + }, + "Next evolution(s)": [ + { + "Number": "094", + "Name": "Gengar" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Shadow Ball", + "Sludge Bomb" + ], + "BaseAttack": 172, + "BaseDefense": 90, + "BaseStamina": 118, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "094", + "Name": "Gengar", + "Classification": "Shadow Pokemon", + "Type I": [ + "Ghost" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Shadow Claw", + "Sucker Punch" + ], + "Weight": "40.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "092", + "Name": "Gastly" + }, + { + "Number": "093", + "Name": "Haunter" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Shadow Ball", + "Sludge Wave" + ], + "BaseAttack": 204, + "BaseDefense": 120, + "BaseStamina": 156, + "CaptureRate": 0.08, + "FleeRate": 0.05 + }, + { + "Number": "095", + "Name": "Onix", + "Classification": "Rock Snake Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Rock Throw", + "Tackle" + ], + "Weight": "210.0 kg", + "Height": "8.8 m", + "Special Attack(s)": [ + "Iron Head", + "Rock Slide", + "Stone Edge" + ], + "BaseAttack": 90, + "BaseDefense": 70, + "BaseStamina": 186, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "096", + "Name": "Drowzee", + "Classification": "Hypnosis Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Pound" + ], + "Weight": "32.4 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 96, + "Name": "Drowzee candies" + }, + "Next evolution(s)": [ + { + "Number": "097", + "Name": "Hypno" + } + ], + "Special Attack(s)": [ + "Psybeam", + "Psychic", + "Psyshock" + ], + "BaseAttack": 104, + "BaseDefense": 120, + "BaseStamina": 140, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "097", + "Name": "Hypno", + "Classification": "Hypnosis Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Zen Headbutt" + ], + "Weight": "75.6 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "096", + "Name": "Drowzee" + } + ], + "Special Attack(s)": [ + "Psychic", + "Psyshock", + "Shadow Ball" + ], + "BaseAttack": 162, + "BaseDefense": 170, + "BaseStamina": 196, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "098", + "Name": "Krabby", + "Classification": "River Crab Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "6.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 98, + "Name": "Krabby candies" + }, + "Next evolution(s)": [ + { + "Number": "099", + "Name": "Kingler" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Vice Grip", + "Water Pulse" + ], + "BaseAttack": 116, + "BaseDefense": 60, + "BaseStamina": 110, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "099", + "Name": "Kingler", + "Classification": "Pincer Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Metal Claw", + "Mud Shot" + ], + "Weight": "60.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "098", + "Name": "Krabby" + } + ], + "Special Attack(s)": [ + "Vice Grip", + "Water Pulse", + "X Scissor" + ], + "BaseAttack": 178, + "BaseDefense": 110, + "BaseStamina": 168, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "100", + "Name": "Voltorb", + "Classification": "Ball Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Tackle" + ], + "Weight": "10.4 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 100, + "Name": "Voltorb candies" + }, + "Next evolution(s)": [ + { + "Number": "101", + "Name": "Electrode" + } + ], + "Special Attack(s)": [ + "Discharge", + "Signal Beam", + "Thunderbolt" + ], + "BaseAttack": 102, + "BaseDefense": 80, + "BaseStamina": 124, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "101", + "Name": "Electrode", + "Classification": "Ball Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Tackle" + ], + "Weight": "66.6 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "100", + "Name": "Voltorb" + } + ], + "Special Attack(s)": [ + "Discharge", + "Hyper Beam", + "Thunderbolt" + ], + "BaseAttack": 150, + "BaseDefense": 120, + "BaseStamina": 174, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "102", + "Name": "Exeggcute", + "Classification": "Egg Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion" + ], + "Weight": "2.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 102, + "Name": "Exeggcute candies" + }, + "Next evolution(s)": [ + { + "Number": "103", + "Name": "Exeggutor" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Psychic", + "Seed Bomb" + ], + "BaseAttack": 110, + "BaseDefense": 120, + "BaseStamina": 132, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "103", + "Name": "Exeggutor", + "Classification": "Coconut Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Zen Headbutt" + ], + "Weight": "120.0 kg", + "Height": "2.0 m", + "Previous evolution(s)": [ + { + "Number": "102", + "Name": "Exeggcute" + } + ], + "Special Attack(s)": [ + "Psychic", + "Seed Bomb", + "Solar Beam" + ], + "BaseAttack": 232, + "BaseDefense": 190, + "BaseStamina": 164, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "104", + "Name": "Cubone", + "Classification": "Lonely Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "6.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 104, + "Name": "Cubone candies" + }, + "Next evolution(s)": [ + { + "Number": "105", + "Name": "Marowak" + } + ], + "Special Attack(s)": [ + "Bone Club", + "Bulldoze", + "Dig" + ], + "BaseAttack": 102, + "BaseDefense": 100, + "BaseStamina": 150, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "105", + "Name": "Marowak", + "Classification": "Bone Keeper Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "45.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "104", + "Name": "Cubone" + } + ], + "Special Attack(s)": [ + "Bone Club", + "Dig", + "Earthquake" + ], + "BaseAttack": 140, + "BaseDefense": 120, + "BaseStamina": 202, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "106", + "Name": "Hitmonlee", + "Classification": "Kicking Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Low Kick", + "Rock Smash" + ], + "Weight": "49.8 kg", + "Height": "1.5 m", + "Next evolution(s)": [ + { + "Number": "107", + "Name": "Hitmonchan" + } + ], + "Special Attack(s)": [ + "Low Sweep", + "Stomp", + "Stone Edge" + ], + "BaseAttack": 148, + "BaseDefense": 100, + "BaseStamina": 172, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "107", + "Name": "Hitmonchan", + "Classification": "Punching Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Bullet Punch", + "Rock Smash" + ], + "Weight": "50.2 kg", + "Height": "1.4 m", + "Previous evolution(s)": [ + { + "Number": "106", + "Name": "Hitmonlee" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Fire Punch", + "Ice Punch", + "Thunder Punch" + ], + "BaseAttack": 138, + "BaseDefense": 100, + "BaseStamina": 204, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "108", + "Name": "Lickitung", + "Classification": "Licking Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Lick", + "Zen Headbutt" + ], + "Weight": "65.5 kg", + "Height": "1.2 m", + "Special Attack(s)": [ + "Hyper Beam", + "Power Whip", + "Stomp" + ], + "BaseAttack": 126, + "BaseDefense": 180, + "BaseStamina": 160, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "109", + "Name": "Koffing", + "Classification": "Poison Gas Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Tackle" + ], + "Weight": "1.0 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 109, + "Name": "Koffing candies" + }, + "Next evolution(s)": [ + { + "Number": "110", + "Name": "Weezing" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Sludge", + "Sludge Bomb" + ], + "BaseAttack": 136, + "BaseDefense": 80, + "BaseStamina": 142, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "110", + "Name": "Weezing", + "Classification": "Poison Gas Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Tackle" + ], + "Weight": "9.5 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "109", + "Name": "Koffing" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Shadow Ball", + "Sludge Bomb" + ], + "BaseAttack": 190, + "BaseDefense": 130, + "BaseStamina": 198, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "111", + "Name": "Rhyhorn", + "Classification": "Spikes Pokemon", + "Type I": [ + "Ground" + ], + "Type II": [ + "Rock" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "115.0 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 111, + "Name": "Rhyhorn candies" + }, + "Next evolution(s)": [ + { + "Number": "112", + "Name": "Rhydon" + } + ], + "Special Attack(s)": [ + "Bulldoze", + "Horn Attack", + "Stomp" + ], + "BaseAttack": 110, + "BaseDefense": 160, + "BaseStamina": 116, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "112", + "Name": "Rhydon", + "Classification": "Drill Pokemon", + "Type I": [ + "Ground" + ], + "Type II": [ + "Rock" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "120.0 kg", + "Height": "1.9 m", + "Previous evolution(s)": [ + { + "Number": "111", + "Name": "Rhyhorn" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Megahorn", + "Stone Edge" + ], + "BaseAttack": 166, + "BaseDefense": 210, + "BaseStamina": 160, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "113", + "Name": "Chansey", + "Classification": "Egg Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound", + "Zen Headbutt" + ], + "Weight": "34.6 kg", + "Height": "1.1 m", + "Special Attack(s)": [ + "Dazzling Gleam", + "Psybeam", + "Psychic" + ], + "BaseAttack": 40, + "BaseDefense": 500, + "BaseStamina": 60, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "114", + "Name": "Tangela", + "Classification": "Vine Pokemon", + "Type I": [ + "Grass" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug" + ], + "Fast Attack(s)": [ + "Vine Whip" + ], + "Weight": "35.0 kg", + "Height": "1.0 m", + "Special Attack(s)": [ + "Power Whip", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 164, + "BaseDefense": 130, + "BaseStamina": 152, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "115", + "Name": "Kangaskhan", + "Classification": "Parent Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Low Kick", + "Mud Slap" + ], + "Weight": "80.0 kg", + "Height": "2.2 m", + "Special Attack(s)": [ + "Brick Break", + "Earthquake", + "Stomp" + ], + "BaseAttack": 142, + "BaseDefense": 210, + "BaseStamina": 178, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "116", + "Name": "Horsea", + "Classification": "Dragon Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Water Gun" + ], + "Weight": "8.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 116, + "Name": "Horsea candies" + }, + "Next evolution(s)": [ + { + "Number": "117", + "Name": "Seadra" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Dragon Pulse", + "Flash Cannon" + ], + "BaseAttack": 122, + "BaseDefense": 60, + "BaseStamina": 100, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "117", + "Name": "Seadra", + "Classification": "Dragon Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Dragon Breath", + "Water Gun" + ], + "Weight": "25.0 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "116", + "Name": "Horsea" + } + ], + "Special Attack(s)": [ + "Blizzard", + "Dragon Pulse", + "Hydro Pump" + ], + "BaseAttack": 176, + "BaseDefense": 110, + "BaseStamina": 150, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "118", + "Name": "Goldeen", + "Classification": "Goldfish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Peck" + ], + "Weight": "15.0 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 118, + "Name": "Goldeen candies" + }, + "Next evolution(s)": [ + { + "Number": "119", + "Name": "Seaking" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Horn Attack", + "Water Pulse" + ], + "BaseAttack": 112, + "BaseDefense": 90, + "BaseStamina": 126, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "119", + "Name": "Seaking", + "Classification": "Goldfish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Peck", + "Poison Jab" + ], + "Weight": "39.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "118", + "Name": "Goldeen" + } + ], + "Special Attack(s)": [ + "Drill Run", + "Icy Wind", + "Megahorn" + ], + "BaseAttack": 172, + "BaseDefense": 160, + "BaseStamina": 160, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "120", + "Name": "Staryu", + "Classification": "Starshape Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Water Gun" + ], + "Weight": "34.5 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 120, + "Name": "Staryu candies" + }, + "Next evolution(s)": [ + { + "Number": "120", + "Name": "Staryu" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Power Gem", + "Swift" + ], + "BaseAttack": 130, + "BaseDefense": 60, + "BaseStamina": 128, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "121", + "Name": "Starmie", + "Classification": "Mysterious Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Water Gun" + ], + "Weight": "80.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "121", + "Name": "Starmie" + } + ], + "Special Attack(s)": [ + "Hydro Pump", + "Power Gem", + "Psybeam" + ], + "BaseAttack": 194, + "BaseDefense": 120, + "BaseStamina": 192, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "122", + "Name": "Mr. Mime", + "Classification": "Barrier Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Zen Headbutt" + ], + "Weight": "54.5 kg", + "Height": "1.3 m", + "Special Attack(s)": [ + "Psybeam", + "Psychic", + "Shadow Ball" + ], + "BaseAttack": 154, + "BaseDefense": 80, + "BaseStamina": 196, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "123", + "Name": "Scyther", + "Classification": "Mantis Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Fire", + "Electric", + "Ice", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Steel Wing" + ], + "Weight": "56.0 kg", + "Height": "1.5 m", + "Special Attack(s)": [ + "Bug Buzz", + "Night Slash", + "X Scissor" + ], + "BaseAttack": 176, + "BaseDefense": 140, + "BaseStamina": 180, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "124", + "Name": "Jynx", + "Classification": "Humanshape Pokemon", + "Type I": [ + "Ice" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Fire", + "Bug", + "Rock", + "Ghost", + "Dark", + "Steel" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Pound" + ], + "Weight": "40.6 kg", + "Height": "1.4 m", + "Special Attack(s)": [ + "Draining Kiss", + "Ice Punch", + "Psyshock" + ], + "BaseAttack": 172, + "BaseDefense": 130, + "BaseStamina": 134, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "125", + "Name": "Electabuzz", + "Classification": "Electric Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Low Kick", + "Thunder Shock" + ], + "Weight": "30.0 kg", + "Height": "1.1 m", + "Special Attack(s)": [ + "Thunder", + "Thunder Punch", + "Thunderbolt" + ], + "BaseAttack": 198, + "BaseDefense": 130, + "BaseStamina": 160, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "126", + "Name": "Magmar", + "Classification": "Spitfire Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Karate Chop" + ], + "Weight": "44.5 kg", + "Height": "1.3 m", + "Special Attack(s)": [ + "Fire Blast", + "Fire Punch", + "Flamethrower" + ], + "BaseAttack": 214, + "BaseDefense": 130, + "BaseStamina": 158, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "127", + "Name": "Pinsir", + "Classification": "Stagbeetle Pokemon", + "Type I": [ + "Bug" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Rock Smash" + ], + "Weight": "55.0 kg", + "Height": "1.5 m", + "Special Attack(s)": [ + "Submission", + "Vice Grip", + "X Scissor" + ], + "BaseAttack": 184, + "BaseDefense": 130, + "BaseStamina": 186, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "128", + "Name": "Tauros", + "Classification": "Wild Bull Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Tackle", + "Zen Headbutt" + ], + "Weight": "88.4 kg", + "Height": "1.4 m", + "Special Attack(s)": [ + "Earthquake", + "Horn Attack", + "Iron Head" + ], + "BaseAttack": 148, + "BaseDefense": 150, + "BaseStamina": 184, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "129", + "Name": "Magikarp", + "Classification": "Fish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Splash" + ], + "Weight": "10.0 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 400, + "Family": 129, + "Name": "Magikarp candies" + }, + "Next evolution(s)": [ + { + "Number": "130", + "Name": "Gyarados" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 42, + "BaseDefense": 40, + "BaseStamina": 84, + "CaptureRate": 0.56, + "FleeRate": 0.15 + }, + { + "Number": "130", + "Name": "Gyarados", + "Classification": "Atrocious Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Dragon Breath" + ], + "Weight": "235.0 kg", + "Height": "6.5 m", + "Previous evolution(s)": [ + { + "Number": "129", + "Name": "Magikarp" + } + ], + "Special Attack(s)": [ + "Dragon Pulse", + "Hydro Pump", + "Twister" + ], + "BaseAttack": 192, + "BaseDefense": 190, + "BaseStamina": 196, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "131", + "Name": "Lapras", + "Classification": "Transport Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Ice" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Rock" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Ice Shard" + ], + "Weight": "220.0 kg", + "Height": "2.5 m", + "Special Attack(s)": [ + "Blizzard", + "Dragon Pulse", + "Ice Beam" + ], + "BaseAttack": 186, + "BaseDefense": 260, + "BaseStamina": 190, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "132", + "Name": "Ditto", + "Classification": "Transform Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound" + ], + "Special Attack(s)": [ + "Struggle" + ], + "Weight": "4.0 kg", + "Height": "0.3 m", + "BaseAttack": 110, + "BaseDefense": 96, + "BaseStamina": 110, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "133", + "Name": "Eevee", + "Classification": "Evolution Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Weight": "6.5 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 133, + "Name": "Eevee candies" + }, + "Next evolution(s)": [ + { + "Number": "134", + "Name": "Vaporeon" + }, + { + "Number": "135", + "Name": "Jolteon" + }, + { + "Number": "136", + "Name": "Flareon" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Dig", + "Swift" + ], + "BaseAttack": 114, + "BaseDefense": 110, + "BaseStamina": 128, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "134", + "Name": "Vaporeon", + "Classification": "Bubble Jet Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Water Gun" + ], + "Weight": "29.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "133", + "Name": "Eevee" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Hydro Pump", + "Water Pulse" + ], + "BaseAttack": 186, + "BaseDefense": 260, + "BaseStamina": 168, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "135", + "Name": "Jolteon", + "Classification": "Lightning Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Thunder Shock" + ], + "Weight": "24.5 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "133", + "Name": "Eevee" + } + ], + "Special Attack(s)": [ + "Discharge", + "Thunder", + "Thunderbolt" + ], + "BaseAttack": 192, + "BaseDefense": 130, + "BaseStamina": 174, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "136", + "Name": "Flareon", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember" + ], + "Weight": "25.0 kg", + "Height": "0.9 m", + "Previous evolution(s)": [ + { + "Number": "133", + "Name": "Eevee" + } + ], + "Special Attack(s)": [ + "Fire Blast", + "Flamethrower", + "Heat Wave" + ], + "BaseAttack": 238, + "BaseDefense": 130, + "BaseStamina": 178, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "137", + "Name": "Porygon", + "Classification": "Virtual Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Weight": "36.5 kg", + "Height": "0.8 m", + "Special Attack(s)": [ + "Discharge", + "Psybeam", + "Signal Beam" + ], + "BaseAttack": 156, + "BaseDefense": 130, + "BaseStamina": 158, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "138", + "Name": "Omanyte", + "Classification": "Spiral Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Water Gun" + ], + "Weight": "7.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 138, + "Name": "Omanyte candies" + }, + "Next evolution(s)": [ + { + "Number": "139", + "Name": "Omastar" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Brine", + "Rock Tomb" + ], + "BaseAttack": 132, + "BaseDefense": 70, + "BaseStamina": 160, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "139", + "Name": "Omastar", + "Classification": "Spiral Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Rock Throw", + "Water Gun" + ], + "Weight": "35.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "138", + "Name": "Omanyte" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Hydro Pump", + "Rock Slide" + ], + "BaseAttack": 180, + "BaseDefense": 140, + "BaseStamina": 202, + "CaptureRate": 0.12, + "FleeRate": 0.05 + }, + { + "Number": "140", + "Name": "Kabuto", + "Classification": "Shellfish Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Scratch" + ], + "Weight": "11.5 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 140, + "Name": "Kabuto candies" + }, + "Next evolution(s)": [ + { + "Number": "141", + "Name": "Kabutops" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Aqua Jet", + "Rock Tomb" + ], + "BaseAttack": 148, + "BaseDefense": 60, + "BaseStamina": 142, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "141", + "Name": "Kabutops", + "Classification": "Shellfish Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Mud Shot" + ], + "Weight": "40.5 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "140", + "Name": "Kabuto" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Stone Edge", + "Water Pulse" + ], + "BaseAttack": 190, + "BaseDefense": 120, + "BaseStamina": 190, + "CaptureRate": 0.12, + "FleeRate": 0.05 + }, + { + "Number": "142", + "Name": "Aerodactyl", + "Classification": "Fossil Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Water", + "Electric", + "Ice", + "Rock", + "Steel" + ], + "Fast Attack(s)": [ + "Bite", + "Steel Wing" + ], + "Weight": "59.0 kg", + "Height": "1.8 m", + "Special Attack(s)": [ + "Ancient Power", + "Hyper Beam", + "Iron Head" + ], + "BaseAttack": 182, + "BaseDefense": 160, + "BaseStamina": 162, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "143", + "Name": "Snorlax", + "Classification": "Sleeping Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Lick", + "Zen Headbutt" + ], + "Weight": "460.0 kg", + "Height": "2.1 m", + "Special Attack(s)": [ + "Body Slam", + "Earthquake", + "Hyper Beam" + ], + "BaseAttack": 180, + "BaseDefense": 320, + "BaseStamina": 180, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "144", + "Name": "Articuno", + "Classification": "Freeze Pokemon", + "Type I": [ + "Ice" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Fire", + "Electric", + "Rock", + "Steel" + ], + "Fast Attack(s)": [ + "Frost Breath" + ], + "Special Attack(s)": [ + "Blizzard", + "Ice Beam", + "Icy Wind" + ], + "Weight": "55.4 kg", + "Height": "1.7 m", + "BaseAttack": 198, + "BaseDefense": 180, + "BaseStamina": 242, + "CaptureRate": 0.0, + "FleeRate": 0.1 + }, + { + "Number": "145", + "Name": "Zapdos", + "Classification": "Electric Pokemon", + "Type I": [ + "Electric" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Ice", + "Rock" + ], + "Fast Attack(s)": [ + "Thunder Shock" + ], + "Special Attack(s)": [ + "Discharge", + "Thunder", + "Thunderbolt" + ], + "Weight": "52.6 kg", + "Height": "1.6 m", + "BaseAttack": 232, + "BaseDefense": 180, + "BaseStamina": 194, + "CaptureRate": 0.0, + "FleeRate": 0.1 + }, + { + "Number": "146", + "Name": "Moltres", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Water", + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Ember" + ], + "Special Attack(s)": [ + "Fire Blast", + "Flamethrower", + "Heat Wave" + ], + "Weight": "60.0 kg", + "Height": "2.0 m", + "BaseAttack": 242, + "BaseDefense": 180, + "BaseStamina": 194, + "CaptureRate": 0.0, + "FleeRate": 0.1 + }, + { + "Number": "147", + "Name": "Dratini", + "Classification": "Dragon Pokemon", + "Type I": [ + "Dragon" + ], + "Weaknesses": [ + "Ice", + "Dragon", + "Fairy" + ], + "Fast Attack(s)": [ + "Dragon Breath" + ], + "Weight": "3.3 kg", + "Height": "1.8 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 147, + "Name": "Dratini candies" + }, + "Special Attack(s)": [ + "Aqua Tail", + "Twister", + "Wrap" + ], + "BaseAttack": 128, + "BaseDefense": 82, + "BaseStamina": 110, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "148", + "Name": "Dragonair", + "Classification": "Dragon Pokemon", + "Type I": [ + "Dragon" + ], + "Weaknesses": [ + "Ice", + "Dragon", + "Fairy" + ], + "Fast Attack(s)": [ + "Dragon Breath" + ], + "Weight": "16.5 kg", + "Height": "4.0 m", + "Previous evolution(s)": [ + { + "Number": "147", + "Name": "Dratini" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 147, + "Name": "Dratini candies" + }, + "Next evolution(s)": [ + { + "Number": "149", + "Name": "Dragonite" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Dragon Pulse", + "Wrap" + ], + "BaseAttack": 170, + "BaseDefense": 122, + "BaseStamina": 152, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "149", + "Name": "Dragonite", + "Classification": "Dragon Pokemon", + "Type I": [ + "Dragon" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Ice", + "Rock", + "Dragon", + "Fairy" + ], + "Fast Attack(s)": [ + "Dragon Breath", + "Steel Wing" + ], + "Weight": "210.0 kg", + "Height": "2.2 m", + "Previous evolution(s)": [ + { + "Number": "147", + "Name": "Dratini" + }, + { + "Number": "148", + "Name": "Dragonair" + } + ], + "Special Attack(s)": [ + "Dragon Claw", + "Dragon Pulse", + "Hyper Beam" + ], + "BaseAttack": 250, + "BaseDefense": 182, + "BaseStamina": 212, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "150", + "Name": "Mewtwo", + "Classification": "Genetic Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Psycho Cut" + ], + "Special Attack(s)": [ + "Hyper Beam", + "Psychic", + "Shadow Ball" + ], + "Weight": "122.0 kg", + "Height": "2.0 m", + "BaseAttack": 284, + "BaseDefense": 212, + "BaseStamina": 202, + "CaptureRate": 0.0, + "FleeRate": 0.1 + }, + { + "Number": "151", + "Name": "Mew", + "Classification": "New Species Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Pound" + ], + "Special Attack(s)": [ + "Dragon Pulse", + "Earthquake", + "Fire Blast", + "Hurricane", + "Hyper Beam", + "Moonblast", + "Psychic", + "Solar Beam", + "Thunder" + ], + "Weight": "4.0 kg", + "Height": "0.4 m", + "BaseAttack": 220, + "BaseDefense": 200, + "BaseStamina": 220, + "CaptureRate": 0.0, + "FleeRate": 0.1 + } +] From 2b31f93d2293e35f7cdb90b359707b5e62f4d897 Mon Sep 17 00:00:00 2001 From: Quantra Date: Tue, 9 Aug 2016 19:36:27 +0100 Subject: [PATCH 064/143] Made paths to .json files absolute so pokecli.py can be called from CRON (#3157) * Made paths to .json files absolute so pokecli.py can be called from CRON * made file paths abs in inventory fixed incorrect dict reference, changed to .get() as felt this was intended --- pokecli.py | 3 +- pokemongo_bot/__init__.py | 33 ++++++++++--------- pokemongo_bot/base_dir.py | 4 +++ .../cell_workers/catch_visible_pokemon.py | 4 ++- .../cell_workers/move_to_map_pokemon.py | 5 +-- pokemongo_bot/cell_workers/recycle_items.py | 3 +- .../cell_workers/transfer_pokemon.py | 4 ++- pokemongo_bot/inventory.py | 7 ++-- 8 files changed, 38 insertions(+), 25 deletions(-) create mode 100644 pokemongo_bot/base_dir.py diff --git a/pokecli.py b/pokecli.py index 55afa55399..6811da7696 100644 --- a/pokecli.py +++ b/pokecli.py @@ -39,6 +39,7 @@ from geopy.exc import GeocoderQuotaExceeded from pokemongo_bot import PokemonGoBot, TreeConfigBuilder +from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.health_record import BotEvent from pokemongo_bot.plugin_loader import PluginLoader @@ -162,7 +163,7 @@ def report_summary(bot): def init_config(): parser = argparse.ArgumentParser() - config_file = "configs/config.json" + config_file = os.path.join(_base_dir, 'configs', 'config.json') web_dir = "web" # If config file exists, load variables from json diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index dd4de22551..133bb4631e 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -28,6 +28,7 @@ from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler from pokemongo_bot.socketio_server.runner import SocketIoRunner from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl +from pokemongo_bot.base_dir import _base_dir from worker_result import WorkerResult from tree_config_builder import ConfigException, MismatchTaskApiVersion, TreeConfigBuilder from inventory import init_inventory @@ -57,9 +58,9 @@ def __init__(self, config): self.config = config self.fort_timeouts = dict() self.pokemon_list = json.load( - open(os.path.join('data', 'pokemon.json')) + open(os.path.join(_base_dir, 'data', 'pokemon.json')) ) - self.item_list = json.load(open(os.path.join('data', 'items.json'))) + self.item_list = json.load(open(os.path.join(_base_dir, 'data', 'items.json'))) self.metrics = Metrics(self) self.latest_inventory = None self.cell = None @@ -109,12 +110,12 @@ def _setup_event_system(self): self.event_manager.event_report() sys.exit(1) - # Registering event: - # self.event_manager.register_event("location", parameters=['lat', 'lng']) - # - # Emitting event should be enough to add logging and send websocket - # message: : - # self.event_manager.emit('location', 'level'='info', data={'lat': 1, 'lng':1}), + # Registering event: + # self.event_manager.register_event("location", parameters=['lat', 'lng']) + # + # Emitting event should be enough to add logging and send websocket + # message: : + # self.event_manager.emit('location', 'level'='info', data={'lat': 1, 'lng':1}), def _register_events(self): self.event_manager.register_event( @@ -498,12 +499,12 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): location = self.position[0:2] cells = self.find_close_cells(*location) - user_data_cells = "data/cells-%s.json" % self.config.username + user_data_cells = os.path.join(_base_dir, 'data', 'cells-%s.json' % self.config.username) with open(user_data_cells, 'w') as outfile: json.dump(cells, outfile) user_web_location = os.path.join( - 'web', 'location-%s.json' % self.config.username + _base_dir, 'web', 'location-%s.json' % self.config.username ) # alt is unused atm but makes using *location easier try: @@ -518,7 +519,7 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): self.logger.info('[x] Error while opening location file: %s' % e) user_data_lastlocation = os.path.join( - 'data', 'last-location-%s.json' % self.config.username + _base_dir, 'data', 'last-location-%s.json' % self.config.username ) try: with open(user_data_lastlocation, 'w') as outfile: @@ -790,7 +791,7 @@ def current_inventory(self): inventory_dict = inventory_req['responses']['GET_INVENTORY'][ 'inventory_delta']['inventory_items'] - user_web_inventory = 'web/inventory-%s.json' % self.config.username + user_web_inventory = os.path.join(_base_dir, 'web', 'inventory-%s.json' % self.config.username) with open(user_web_inventory, 'w') as outfile: json.dump(inventory_dict, outfile) @@ -901,8 +902,8 @@ def _set_starting_position(self): level='debug', formatted='Loading cached location...' ) - with open('data/last-location-%s.json' % - self.config.username) as f: + with open(os.path.join(_base_dir, 'data', 'last-location-%s.json' % + self.config.username)) as f: location_json = json.load(f) location = ( location_json['lat'], @@ -1051,8 +1052,8 @@ def has_space_for_loot(self): def get_forts(self, order_by_distance=False): forts = [fort - for fort in self.cell['forts'] - if 'latitude' in fort and 'type' in fort] + for fort in self.cell['forts'] + if 'latitude' in fort and 'type' in fort] if order_by_distance: forts.sort(key=lambda x: distance( diff --git a/pokemongo_bot/base_dir.py b/pokemongo_bot/base_dir.py new file mode 100644 index 0000000000..83978c964a --- /dev/null +++ b/pokemongo_bot/base_dir.py @@ -0,0 +1,4 @@ +import os + + +_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py index 654c2467b3..0203459c28 100644 --- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -1,9 +1,11 @@ import json +import os from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker from utils import distance from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.base_dir import _base_dir class CatchVisiblePokemon(BaseTask): @@ -27,7 +29,7 @@ def work(self): key= lambda x: distance(self.bot.position[0], self.bot.position[1], x['latitude'], x['longitude']) ) - user_web_catchable = 'web/catchable-{}.json'.format(self.bot.config.username) + user_web_catchable = os.path.join(_base_dir, 'web', 'catchable-{}.json'.format(self.bot.config.username)) for pokemon in self.bot.cell['catchable_pokemons']: with open(user_web_catchable, 'w') as outfile: json.dump(pokemon, outfile) diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 2cfd45d14b..7ff0eb1cf4 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -54,6 +54,7 @@ import json import base64 import requests +from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.cell_workers.utils import distance, format_dist, format_time from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult @@ -84,7 +85,7 @@ def initialize(self): self.caught = [] self.min_ball = self.config.get('min_ball', 1) - data_file = 'data/map-caught-{}.json'.format(self.bot.config.username) + data_file = os.path.join(_base_dir, 'map-caught-{}.json'.format(self.bot.config.username)) if os.path.isfile(data_file): self.caught = json.load( open(data_file) @@ -222,7 +223,7 @@ def snipe(self, pokemon): return WorkerResult.SUCCESS def dump_caught_pokemon(self): - user_data_map_caught = 'data/map-caught-{}.json'.format(self.bot.config.username) + user_data_map_caught = os.path.join(_base_dir, 'data', 'map-caught-{}.json'.format(self.bot.config.username)) with open(user_data_map_caught, 'w') as outfile: json.dump(self.caught, outfile) diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 673b373fba..15c8f7931a 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -1,5 +1,6 @@ import json import os +from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.base_task import BaseTask from pokemongo_bot.tree_config_builder import ConfigException @@ -13,7 +14,7 @@ def initialize(self): self._validate_item_filter() def _validate_item_filter(self): - item_list = json.load(open(os.path.join('data', 'items.json'))) + item_list = json.load(open(os.path.join(_base_dir, 'data', 'items.json'))) for config_item_name, bag_count in self.item_filter.iteritems(): if config_item_name not in item_list.viewvalues(): if config_item_name not in item_list: diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index ebc197ef24..88548e75b2 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -1,5 +1,7 @@ import json +import os +from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.human_behaviour import action_delay from pokemongo_bot.base_task import BaseTask @@ -83,7 +85,7 @@ def _release_pokemon_get_groups(self): inventory_dict = inventory_req['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] - user_web_inventory = 'web/inventory-%s.json' % (self.bot.config.username) + user_web_inventory = os.path.join(_base_dir, 'web', 'inventory-%s.json' % (self.bot.config.username)) with open(user_web_inventory, 'w') as outfile: json.dump(inventory_dict, outfile) diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index c74a85296f..1fc208d235 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1,5 +1,6 @@ import json import os +from pokemongo_bot.base_dir import _base_dir ''' Helper class for updating/retrieving Inventory data @@ -42,7 +43,7 @@ def refresh(self, inventory): self._data = self.retrieve_data(inventory) def get(self, id): - return self._data(id) + return self._data.get(id) def all(self): return list(self._data.values()) @@ -96,7 +97,7 @@ def captured(self, pokemon_id): class Items(_BaseInventoryComponent): TYPE = 'item' ID_FIELD = 'item_id' - STATIC_DATA_FILE = os.path.join('data', 'items.json') + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'items.json') def count_for(self, item_id): return self._data[item_id]['count'] @@ -105,7 +106,7 @@ def count_for(self, item_id): class Pokemons(_BaseInventoryComponent): TYPE = 'pokemon_data' ID_FIELD = 'id' - STATIC_DATA_FILE = os.path.join('data', 'pokemon.json') + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'pokemon.json') def parse(self, item): if 'is_egg' in item: From ceb5db71cb1d00ebdd62b2a4f93cc4b18a44ccdb Mon Sep 17 00:00:00 2001 From: Amal Samally Date: Tue, 9 Aug 2016 23:08:00 +0400 Subject: [PATCH 065/143] Add fast & charged moves data from #2117 (originally by @iananass) (#3336) Data for pokemon quick & slow attacks --- data/charged_moves.json | 92 +++++++++++++++++++++++++++++++++++++++++ data/fast_moves.json | 41 ++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 data/charged_moves.json create mode 100644 data/fast_moves.json diff --git a/data/charged_moves.json b/data/charged_moves.json new file mode 100644 index 0000000000..3d487b7201 --- /dev/null +++ b/data/charged_moves.json @@ -0,0 +1,92 @@ +[{"id":32,"name":"Stone Edge","type":"Rock","damage":80,"duration":3100,"energy":100,"dps":25.8}, +{"id":28,"name":"Cross Chop","type":"Fighting","damage":60,"duration":2000,"energy":100,"dps":30.0}, +{"id":83,"name":"Dragon Claw","type":"Dragon","damage":35,"duration":1500,"energy":50,"dps":23.33}, +{"id":40,"name":"Blizzard","type":"Ice","damage":100,"duration":3900,"energy":100,"dps":25.64}, +{"id":131,"name":"Body Slam","type":"Normal","damage":40,"duration":1560,"energy":50,"dps":25.64}, +{"id":22,"name":"Megahorn","type":"Bug","damage":80,"duration":3200,"energy":100,"dps":25.0}, +{"id":122,"name":"Hurricane","type":"Flying","damage":80,"duration":3200,"energy":100,"dps":25.0}, +{"id":116,"name":"Solar Beam","type":"Grass","damage":120,"duration":4900,"energy":100,"dps":24.48}, +{"id":103,"name":"Fire Blast","type":"Fire","damage":100,"duration":4100,"energy":100,"dps":24.39}, +{"id":14,"name":"Hyper Beam","type":"Normal","damage":120,"duration":5000,"energy":100,"dps":24.0}, +{"id":31,"name":"Earthquake","type":"Ground","damage":100,"duration":4200,"energy":100,"dps":23.8}, +{"id":118,"name":"Power Whip","type":"Grass","damage":70,"duration":2800,"energy":100,"dps":25.0}, +{"id":107,"name":"Hydro Pump","type":"Water","damage":90,"duration":3800,"energy":100,"dps":23.68}, +{"id":117,"name":"Leaf Blade","type":"Grass","damage":55,"duration":2800,"energy":50,"dps":19.64}, +{"id":78,"name":"Thunder","type":"Electric","damage":100,"duration":4300,"energy":100,"dps":23.25}, +{"id":123,"name":"Brick Break","type":"Fighting","damage":30,"duration":1600,"energy":33,"dps":18.75}, +{"id":92,"name":"Gunk Shot","type":"Poison","damage":65,"duration":3000,"energy":100,"dps":21.66}, +{"id":90,"name":"Sludge Bomb","type":"Poison","damage":55,"duration":2600,"energy":50,"dps":21.15}, +{"id":42,"name":"Heat Wave","type":"Fire","damage":80,"duration":3800,"energy":100,"dps":21.05}, +{"id":87,"name":"Moonblast","type":"Fairy","damage":85,"duration":4100,"energy":100,"dps":20.73}, +{"id":91,"name":"Sludge Wave","type":"Poison","damage":70,"duration":3400,"energy":100,"dps":20.58}, +{"id":79,"name":"Thunderbolt","type":"Electric","damage":55,"duration":2700,"energy":50,"dps":20.37}, +{"id":47,"name":"Petal Blizzard","type":"Grass","damage":65,"duration":3200,"energy":50,"dps":20.31}, +{"id":89,"name":"Cross Poison","type":"Poison","damage":25,"duration":1500,"energy":25,"dps":16.66}, +{"id":108,"name":"Psychic","type":"Psychic","damage":55,"duration":2800,"energy":50,"dps":19.64}, +{"id":58,"name":"Aqua Tail","type":"Water","damage":45,"duration":2350,"energy":50,"dps":19.14}, +{"id":24,"name":"Flamethrower","type":"Fire","damage":55,"duration":2900,"energy":50,"dps":18.96}, +{"id":88,"name":"Play Rough","type":"Fairy","damage":55,"duration":2900,"energy":50,"dps":18.96}, +{"id":82,"name":"Dragon Pulse","type":"Dragon","damage":65,"duration":3600,"energy":50,"dps":18.05}, +{"id":39,"name":"Ice Beam","type":"Ice","damage":65,"duration":3650,"energy":50,"dps":17.8}, +{"id":49,"name":"Bug Buzz","type":"Bug","damage":75,"duration":4250,"energy":50,"dps":17.64}, +{"id":46,"name":"Drill Run","type":"Ground","damage":50,"duration":3400,"energy":33,"dps":14.7}, +{"id":59,"name":"Seed Bomb","type":"Grass","damage":40,"duration":2400,"energy":33,"dps":16.66}, +{"id":77,"name":"Thunder Punch","type":"Electric","damage":40,"duration":2400,"energy":33,"dps":16.66}, +{"id":100,"name":"X Scissor","type":"Bug","damage":35,"duration":2100,"energy":33,"dps":16.66}, +{"id":129,"name":"Hyper Fang","type":"Normal","damage":35,"duration":2100,"energy":33,"dps":16.66}, +{"id":64,"name":"Rock Slide","type":"Rock","damage":50,"duration":3200,"energy":33,"dps":15.62}, +{"id":94,"name":"Bone Club","type":"Ground","damage":25,"duration":1600,"energy":25,"dps":15.62}, +{"id":36,"name":"Flash Cannon","type":"Steel","damage":60,"duration":3900,"energy":33,"dps":15.38}, +{"id":74,"name":"Iron Head","type":"Steel","damage":30,"duration":2000,"energy":33,"dps":15.0}, +{"id":38,"name":"Drill Peck","type":"Flying","damage":40,"duration":2700,"energy":33,"dps":14.81}, +{"id":60,"name":"Psyshock","type":"Psychic","damage":40,"duration":2700,"energy":33,"dps":14.81}, +{"id":70,"name":"Shadow Ball","type":"Ghost","damage":45,"duration":3080,"energy":33,"dps":14.61}, +{"id":99,"name":"Signal Beam","type":"Bug","damage":45,"duration":3100,"energy":33,"dps":14.51}, +{"id":115,"name":"Fire Punch","type":"Fire","damage":40,"duration":2800,"energy":33,"dps":14.28}, +{"id":54,"name":"Submission","type":"Fighting","damage":30,"duration":2100,"energy":33,"dps":14.28}, +{"id":102,"name":"Flame Burst","type":"Fire","damage":30,"duration":2100,"energy":25,"dps":14.28}, +{"id":127,"name":"Stomp","type":"Normal","damage":30,"duration":2100,"energy":25,"dps":14.28}, +{"id":35,"name":"Discharge","type":"Electric","damage":35,"duration":2500,"energy":33,"dps":14.0}, +{"id":65,"name":"Power Gem","type":"Rock","damage":40,"duration":2900,"energy":33,"dps":13.79}, +{"id":106,"name":"Scald","type":"Water","damage":55,"duration":4000,"energy":33,"dps":13.75}, +{"id":109,"name":"Psystrike","type":"Psychic","damage":70,"duration":5100,"energy":100,"dps":13.72}, +{"id":56,"name":"Low Sweep","type":"Fighting","damage":30,"duration":2250,"energy":25,"dps":13.33}, +{"id":51,"name":"Night Slash","type":"Dark","damage":30,"duration":2700,"energy":25,"dps":11.11}, +{"id":86,"name":"Dazzling Gleam","type":"Fairy","damage":55,"duration":4200,"energy":33,"dps":13.09}, +{"id":16,"name":"Dark Pulse","type":"Dark","damage":45,"duration":3500,"energy":33,"dps":12.85}, +{"id":33,"name":"Ice Punch","type":"Ice","damage":45,"duration":3500,"energy":33,"dps":12.85}, +{"id":26,"name":"Dig","type":"Ground","damage":70,"duration":5800,"energy":33,"dps":12.06}, +{"id":20,"name":"Vice Grip","type":"Normal","damage":25,"duration":2100,"energy":20,"dps":11.9}, +{"id":18,"name":"Sludge","type":"Poison","damage":30,"duration":2600,"energy":25,"dps":11.53}, +{"id":96,"name":"Mud Bomb","type":"Ground","damage":30,"duration":2600,"energy":25,"dps":11.53}, +{"id":126,"name":"Horn Attack","type":"Normal","damage":25,"duration":2200,"energy":25,"dps":11.36}, +{"id":121,"name":"Air Cutter","type":"Flying","damage":30,"duration":3300,"energy":25,"dps":9.09}, +{"id":132,"name":"Rest","type":"Normal","damage":35,"duration":3100,"energy":33,"dps":11.29}, +{"id":72,"name":"Magnet Bomb","type":"Steel","damage":30,"duration":2800,"energy":25,"dps":10.71}, +{"id":57,"name":"Aqua Jet","type":"Water","damage":25,"duration":2350,"energy":20,"dps":10.63}, +{"id":105,"name":"Water Pulse","type":"Water","damage":35,"duration":3300,"energy":25,"dps":10.6}, +{"id":30,"name":"Psybeam","type":"Psychic","damage":40,"duration":3800,"energy":25,"dps":10.52}, +{"id":63,"name":"Rock Tomb","type":"Rock","damage":30,"duration":3400,"energy":25,"dps":8.82}, +{"id":50,"name":"Poison Fang","type":"Poison","damage":25,"duration":2400,"energy":20,"dps":10.41}, +{"id":104,"name":"Brine","type":"Water","damage":25,"duration":2400,"energy":25,"dps":10.41}, +{"id":45,"name":"Aerial Ace","type":"Flying","damage":30,"duration":2900,"energy":25,"dps":10.34}, +{"id":53,"name":"Bubble Beam","type":"Water","damage":30,"duration":2900,"energy":25,"dps":10.34}, +{"id":95,"name":"Bulldoze","type":"Ground","damage":35,"duration":3400,"energy":25,"dps":10.29}, +{"id":125,"name":"Swift","type":"Normal","damage":30,"duration":3000,"energy":25,"dps":10.0}, +{"id":62,"name":"Ancient Power","type":"Rock","damage":35,"duration":3600,"energy":25,"dps":9.72}, +{"id":114,"name":"Giga Drain","type":"Grass","damage":35,"duration":3600,"energy":33,"dps":9.72}, +{"id":69,"name":"Ominous Wind","type":"Ghost","damage":30,"duration":3100,"energy":25,"dps":9.67}, +{"id":67,"name":"Shadow Punch","type":"Ghost","damage":20,"duration":2100,"energy":25,"dps":9.52}, +{"id":80,"name":"Twister","type":"Dragon","damage":25,"duration":2700,"energy":20,"dps":9.25}, +{"id":85,"name":"Draining Kiss","type":"Fairy","damage":25,"duration":2800,"energy":20,"dps":8.92}, +{"id":21,"name":"Flame Wheel","type":"Fire","damage":40,"duration":4600,"energy":25,"dps":8.69}, +{"id":133,"name":"Struggle","type":"Normal","damage":15,"duration":1695,"energy":20,"dps":8.84}, +{"id":101,"name":"Flame Charge","type":"Fire","damage":25,"duration":3100,"energy":20,"dps":8.06}, +{"id":34,"name":"Heart Stamp","type":"Psychic","damage":20,"duration":2550,"energy":25,"dps":7.84}, +{"id":75,"name":"Parabolic Charge","type":"Electric","damage":15,"duration":2100,"energy":20,"dps":7.14}, +{"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":3700,"energy":20,"dps":6.75}, +{"id":111,"name":"Icy Wind","type":"Ice","damage":25,"duration":3800,"energy":20,"dps":6.57}, +{"id":84,"name":"Disarming Voice","type":"Fairy","damage":25,"duration":3900,"energy":20,"dps":6.41}, +{"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":4000,"energy":20,"dps":6.25}, +{"id":66,"name":"Shadow Sneak","type":"Ghost","damage":15,"duration":3100,"energy":20,"dps":4.83}, +{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] \ No newline at end of file diff --git a/data/fast_moves.json b/data/fast_moves.json new file mode 100644 index 0000000000..ec16d2cc8a --- /dev/null +++ b/data/fast_moves.json @@ -0,0 +1,41 @@ +[{"id":222,"name":"Pound","type":"Normal","damage":7,"duration":540,"energy":7,"dps":12.96}, +{"id":228,"name":"Metal Claw","type":"Steel","damage":8,"duration":630,"energy":7,"dps":12.69}, +{"id":226,"name":"Psycho Cut","type":"Psychic","damage":7,"duration":570,"energy":7,"dps":12.28}, +{"id":210,"name":"Wing Attack","type":"Flying","damage":9,"duration":750,"energy":7,"dps":12.0}, +{"id":202,"name":"Bite","type":"Dark","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":204,"name":"Dragon Breath","type":"Dragon","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":220,"name":"Scratch","type":"Normal","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":230,"name":"Water Gun","type":"Water","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":240,"name":"Fire Fang","type":"Fire","damage":10,"duration":840,"energy":4,"dps":11.9}, +{"id":213,"name":"Shadow Claw","type":"Ghost","damage":11,"duration":950,"energy":7,"dps":11.57}, +{"id":238,"name":"Feint Attack","type":"Dark","damage":12,"duration":1040,"energy":7,"dps":11.53}, +{"id":224,"name":"Poison Jab","type":"Poison","damage":12,"duration":1050,"energy":7,"dps":11.42}, +{"id":234,"name":"Zen Headbutt","type":"Psychic","damage":12,"duration":1050,"energy":4,"dps":11.42}, +{"id":239,"name":"Steel Wing","type":"Steel","damage":15,"duration":1330,"energy":4,"dps":11.27}, +{"id":201,"name":"Bug Bite","type":"Bug","damage":5,"duration":450,"energy":7,"dps":11.11}, +{"id":218,"name":"Frost Breath","type":"Ice","damage":9,"duration":810,"energy":7,"dps":11.11}, +{"id":233,"name":"Mud Slap","type":"Ground","damage":15,"duration":1350,"energy":9,"dps":11.11}, +{"id":216,"name":"Mud Shot","type":"Ground","damage":6,"duration":550,"energy":7,"dps":10.9}, +{"id":221,"name":"Tackle","type":"Normal","damage":12,"duration":1100,"energy":7,"dps":10.9}, +{"id":237,"name":"Bubble","type":"Water","damage":25,"duration":2300,"energy":15,"dps":10.86}, +{"id":214,"name":"Vine Whip","type":"Grass","damage":7,"duration":650,"energy":7,"dps":10.76}, +{"id":217,"name":"Ice Shard","type":"Ice","damage":15,"duration":1400,"energy":7,"dps":10.71}, +{"id":241,"name":"Rock Smash","type":"Fighting","damage":15,"duration":1410,"energy":7,"dps":10.63}, +{"id":223,"name":"Cut","type":"Normal","damage":12,"duration":1130,"energy":7,"dps":10.61}, +{"id":236,"name":"Poison Sting","type":"Poison","damage":6,"duration":575,"energy":4,"dps":10.43}, +{"id":215,"name":"Razor Leaf","type":"Grass","damage":15,"duration":1450,"energy":7,"dps":10.34}, +{"id":212,"name":"Lick","type":"Ghost","damage":5,"duration":500,"energy":7,"dps":10.0}, +{"id":206,"name":"Spark","type":"Electric","damage":7,"duration":700,"energy":4,"dps":10.0}, +{"id":203,"name":"Sucker Punch","type":"Dark","damage":7,"duration":700,"energy":4,"dps":10.0}, +{"id":235,"name":"Confusion","type":"Psychic","damage":15,"duration":1510,"energy":7,"dps":9.93}, +{"id":225,"name":"Acid","type":"Poison","damage":10,"duration":1050,"energy":7,"dps":9.52}, +{"id":209,"name":"Ember","type":"Fire","damage":10,"duration":1050,"energy":7,"dps":9.52}, +{"id":227,"name":"Rock Throw","type":"Rock","damage":12,"duration":1360,"energy":7,"dps":8.82}, +{"id":211,"name":"Peck","type":"Flying","damage":10,"duration":1150,"energy":10,"dps":8.69}, +{"id":207,"name":"Low Kick","type":"Fighting","damage":5,"duration":600,"energy":7,"dps":8.33}, +{"id":205,"name":"Thunder Shock","type":"Electric","damage":5,"duration":600,"energy":7,"dps":8.33}, +{"id":229,"name":"Bullet Punch","type":"Steel","damage":10,"duration":1200,"energy":7,"dps":8.33}, +{"id":219,"name":"Quick Attack","type":"Normal","damage":10,"duration":1330,"energy":7,"dps":7.51}, +{"id":200,"name":"Fury Cutter","type":"Bug","damage":3,"duration":400,"energy":12,"dps":7.5}, +{"id":208,"name":"Karate Chop","type":"Fighting","damage":6,"duration":800,"energy":7,"dps":7.5}, +{"id":231,"name":"Splash","type":"Water","damage":0,"duration":1230,"energy":7,"dps":0.0}] \ No newline at end of file From bb2c9522586f82e70011bbc63f0326c765741e1e Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Tue, 9 Aug 2016 12:18:45 -0700 Subject: [PATCH 066/143] Upgrade pgoapi to the b4bf0e089dfe09903f8dda37dae56910e01f94cc commit(latest for now). (#3337) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76b1d15a7b..617f48a76c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy==1.11.0 networkx==1.11 --e git+https://github.com/keyphact/pgoapi.git@a2755eb42dfe49e359798d2f4defefc97fb8163d#egg=pgoapi +-e git+https://github.com/keyphact/pgoapi.git@b4bf0e089dfe09903f8dda37dae56910e01f94cc#egg=pgoapi geopy==1.11.0 protobuf==3.0.0b4 requests==2.10.0 From 9172937009600cae642842f1574e93e7873ed50a Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Tue, 9 Aug 2016 12:19:39 -0700 Subject: [PATCH 067/143] =?UTF-8?q?Revert=20"Upgrade=20pgoapi=20to=20the?= =?UTF-8?q?=20b4bf0e089dfe09903f8dda37dae56910e01f94cc=20commit=E2=80=A6"?= =?UTF-8?q?=20(#3340)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 617f48a76c..76b1d15a7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy==1.11.0 networkx==1.11 --e git+https://github.com/keyphact/pgoapi.git@b4bf0e089dfe09903f8dda37dae56910e01f94cc#egg=pgoapi +-e git+https://github.com/keyphact/pgoapi.git@a2755eb42dfe49e359798d2f4defefc97fb8163d#egg=pgoapi geopy==1.11.0 protobuf==3.0.0b4 requests==2.10.0 From 3519c317fd242b79aeff857dbf4b2532455773d2 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Tue, 9 Aug 2016 12:26:16 -0700 Subject: [PATCH 068/143] Added map_path configuration for move_to_map. (#3339) --- configs/config.json.map.example | 1 + pokemongo_bot/cell_workers/move_to_map_pokemon.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/configs/config.json.map.example b/configs/config.json.map.example index bb6878f5ac..56a006450c 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -66,6 +66,7 @@ "snipe_high_prio_threshold": 400, "update_map": true, "mode": "priority", + "map_path": "raw_data", "catch": { "==========Legendaries==========": 0, "Aerodactyl": 1000, diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 7ff0eb1cf4..efd058ca96 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -84,6 +84,7 @@ def initialize(self): self.unit = self.bot.config.distance_unit self.caught = [] self.min_ball = self.config.get('min_ball', 1) + self.map_path = self.config.get('map_path', 'raw_data') data_file = os.path.join(_base_dir, 'map-caught-{}.json'.format(self.bot.config.username)) if os.path.isfile(data_file): @@ -93,7 +94,7 @@ def initialize(self): def get_pokemon_from_map(self): try: - req = requests.get('{}/raw_data?gyms=false&scanned=false'.format(self.config['address'])) + req = requests.get('{}/{}?gyms=false&scanned=false'.format(self.config['address'], self.map_path)) except requests.exceptions.ConnectionError: self._emit_failure('Could not get Pokemon data from PokemonGo-Map: ' '{}. Is it running?'.format( From b94d3695e454b1020775e4b717fe397e18646fce Mon Sep 17 00:00:00 2001 From: spalacio Date: Tue, 9 Aug 2016 23:32:33 +0200 Subject: [PATCH 069/143] Log stats on terminal (#3312) * added _log_on_terminal function * Added logic to toggle terminal logging functionality from config file * Added possibility to disable title changes, refactor code * Refactor tuples * Refactor ifs to clearer syntax --- .../cell_workers/update_title_stats.py | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index 0a6e3c592d..bc40ed82e8 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -18,10 +18,22 @@ class UpdateTitleStats(BaseTask): "type": "UpdateTitleStats", "config": { "min_interval": 10, - "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"] + "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"], } } + You can set a logging on terminal mode like this: + + Example logging on console (and disabling title change): + { + "type": "UpdateTitleStats", + "config": { + "min_interval": 10, + "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"], + "terminal_log": true, + "terminal_title": false + } + } Available stats : - login : The account login (from the credentials). - username : The trainer name (asked at first in-game connection). @@ -53,8 +65,6 @@ class UpdateTitleStats(BaseTask): """ SUPPORTED_TASK_API_VERSION = 1 - DEFAULT_MIN_INTERVAL = 10 - DEFAULT_DISPLAYED_STATS = [] def __init__(self, bot, config): """ @@ -67,12 +77,14 @@ def __init__(self, bot, config): super(UpdateTitleStats, self).__init__(bot, config) self.next_update = None - self.min_interval = self.DEFAULT_MIN_INTERVAL - self.displayed_stats = self.DEFAULT_DISPLAYED_STATS - self.bot.event_manager.register_event('update_title', parameters=('title')) + self.min_interval = int(self.config.get('min_interval', 120)) + self.displayed_stats = self.config.get('stats', []) + self.terminal_log = self.config.get('terminal_log', False) + self.terminal_title = self.config.get('terminal_title', True) - self._process_config() + self.bot.event_manager.register_event('update_title', parameters=('title',)) + self.bot.event_manager.register_event('log_stats',parameters=('title',)) def initialize(self): pass @@ -89,7 +101,12 @@ def work(self): # If title is empty, it couldn't be generated. if not title: return WorkerResult.SUCCESS - self._update_title(title, _platform) + + if self.terminal_title: + self._update_title(title, _platform) + + if self.terminal_log: + self._log_on_terminal(title) return WorkerResult.SUCCESS def _should_display(self): @@ -100,6 +117,16 @@ def _should_display(self): """ return self.next_update is None or datetime.now() >= self.next_update + def _log_on_terminal(self, title): + self.emit_event( + 'log_stats', + formatted="{title}", + data={ + 'title': title + } + ) + self.next_update = datetime.now() + timedelta(seconds=self.min_interval) + def _update_title(self, title, platform): """ Updates the window title using different methods, according to the given platform @@ -119,7 +146,7 @@ def _update_title(self, title, platform): 'title': title } ) - + if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) stdout.flush() @@ -133,14 +160,6 @@ def _update_title(self, title, platform): self.next_update = datetime.now() + timedelta(seconds=self.min_interval) - def _process_config(self): - """ - Fetches the configuration for this worker and stores the values internally. - :return: Nothing. - :rtype: None - """ - self.min_interval = int(self.config.get('min_interval', self.DEFAULT_MIN_INTERVAL)) - self.displayed_stats = self.config.get('stats', self.DEFAULT_DISPLAYED_STATS) def _get_stats_title(self, player_stats): """ From 2127bef8d7464f1c7fcd0b61dc104f090139e4ec Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Tue, 9 Aug 2016 23:56:29 +0200 Subject: [PATCH 070/143] changes to improve event system based on new web ui devs requests --- pokemongo_bot/__init__.py | 25 +++++++++++++++---- .../cell_workers/pokemon_catch_worker.py | 18 +++++++++++-- pokemongo_bot/socketio_server/app.py | 6 ++--- pokemongo_bot/step_walker.py | 11 ++++++++ pokemongo_bot/websocket_remote_control.py | 9 +++++-- 5 files changed, 57 insertions(+), 12 deletions(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 133bb4631e..7e9e3eaf89 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -234,6 +234,10 @@ def _register_events(self): 'cp', 'iv', 'iv_display', + 'encounter_id', + 'latitude', + 'longitude', + 'pokemon_id' ) ) self.event_manager.register_event('no_pokeballs') @@ -268,7 +272,13 @@ def _register_events(self): ) self.event_manager.register_event( 'pokemon_vanished', - parameters=('pokemon',) + parameters=( + 'pokemon', + 'encounter_id', + 'latitude', + 'longitude', + 'pokemon_id' + ) ) self.event_manager.register_event('pokemon_not_in_range') self.event_manager.register_event('pokemon_inventory_full') @@ -276,7 +286,11 @@ def _register_events(self): 'pokemon_caught', parameters=( 'pokemon', - 'cp', 'iv', 'iv_display', 'exp' + 'cp', 'iv', 'iv_display', 'exp', + 'encounter_id', + 'latitude', + 'longitude', + 'pokemon_id' ) ) self.event_manager.register_event( @@ -988,9 +1002,10 @@ def heartbeat(self): pass def update_web_location_worker(self): - while True: - self.web_update_queue.get() - self.update_web_location() + pass + # while True: + # self.web_update_queue.get() + # self.update_web_location() def get_inventory_count(self, what): response_dict = self.get_inventory() diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index d551a68632..345fcabc7e 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -98,6 +98,10 @@ def work(self, response_dict=None): 'cp': pokemon.cp, 'iv': pokemon.iv, 'iv_display': pokemon.iv_display, + 'encounter_id': self.pokemon['encounter_id'], + 'latitude': self.pokemon['latitude'], + 'longitude': self.pokemon['longitude'], + 'pokemon_id': pokemon.num } ) @@ -370,7 +374,13 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): self.emit_event( 'pokemon_vanished', formatted='{pokemon} vanished!', - data={'pokemon': pokemon.name} + data={ + 'pokemon': pokemon.name, + 'encounter_id': self.pokemon['encounter_id'], + 'latitude': self.pokemon['latitude'], + 'longitude': self.pokemon['longitude'], + 'pokemon_id': pokemon.num + } ) if self._pct(catch_rate_by_ball[current_ball]) == 100: self.bot.softban = True @@ -386,7 +396,11 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): 'cp': pokemon.cp, 'iv': pokemon.iv, 'iv_display': pokemon.iv_display, - 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) + 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']), + 'encounter_id': self.pokemon['encounter_id'], + 'latitude': self.pokemon['latitude'], + 'longitude': self.pokemon['longitude'], + 'pokemon_id': pokemon.num } ) diff --git a/pokemongo_bot/socketio_server/app.py b/pokemongo_bot/socketio_server/app.py index 09c237f910..7f3c5040d2 100644 --- a/pokemongo_bot/socketio_server/app.py +++ b/pokemongo_bot/socketio_server/app.py @@ -26,7 +26,7 @@ def request_reply(sid, response): @sio.on('bot:broadcast') def bot_broadcast(sid, env): - event = env.pop('event') - account = env.pop('account') + event = env['event'] + account = env['account'] event_name = "{}:{}".format(event, account) - sio.emit(event_name, data=env['data']) + sio.emit(event_name, data=env) diff --git a/pokemongo_bot/step_walker.py b/pokemongo_bot/step_walker.py index f6c2cbfe96..7727dc6a0c 100644 --- a/pokemongo_bot/step_walker.py +++ b/pokemongo_bot/step_walker.py @@ -55,6 +55,17 @@ def step(self): cLng = self.initLng + scaledDLng + random_lat_long_delta() self.api.set_position(cLat, cLng, 0) + self.bot.event_manager.emit( + 'position_update', + sender=self, + level='debug', + data={ + 'current_position': (cLat, cLng), + 'last_position': (self.initLat, self.initLng), + 'distance': '', + 'distance_unit': '' + } + ) self.bot.heartbeat() sleep(1) # sleep one second plus a random delta diff --git a/pokemongo_bot/websocket_remote_control.py b/pokemongo_bot/websocket_remote_control.py index c4e15362b6..14645e6572 100644 --- a/pokemongo_bot/websocket_remote_control.py +++ b/pokemongo_bot/websocket_remote_control.py @@ -42,11 +42,16 @@ def on_remote_command(self, command): command_handler() def get_player_info(self): - player_info = self.bot.get_inventory()['responses']['GET_INVENTORY'] + request = self.api.create_request() + request.get_player() + request.get_inventory() + response_dict = request.call() + inventory = response_dict['responses']['GET_INVENTORY'] + player_info = response_dict['responses']['GET_PLAYER'] self.sio.emit( 'bot:send_reply', { - 'result': player_info, + 'result': {'inventory': inventory, 'player': player_info}, 'command': 'get_player_info', 'account': self.bot.config.username } From c556b48aa9a34d5d93e1827784461b42b09c2900 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Wed, 10 Aug 2016 01:34:40 +0200 Subject: [PATCH 071/143] typo :D --- pokemongo_bot/websocket_remote_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/websocket_remote_control.py b/pokemongo_bot/websocket_remote_control.py index 14645e6572..023db22ff4 100644 --- a/pokemongo_bot/websocket_remote_control.py +++ b/pokemongo_bot/websocket_remote_control.py @@ -42,7 +42,7 @@ def on_remote_command(self, command): command_handler() def get_player_info(self): - request = self.api.create_request() + request = self.bot.api.create_request() request.get_player() request.get_inventory() response_dict = request.call() From 9c1a9435daaf547acd069a5c40270191b2a16d60 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Wed, 10 Aug 2016 02:13:21 +0200 Subject: [PATCH 072/143] let's use dict.get a bit to avoid errors --- brantje.py | 9 +++++++++ pokemongo_bot/websocket_remote_control.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 brantje.py diff --git a/brantje.py b/brantje.py new file mode 100644 index 0000000000..0674ceb077 --- /dev/null +++ b/brantje.py @@ -0,0 +1,9 @@ +# coding: utf-8 +from socketIO_client import SocketIO +s = SocketIO('localhost', 4000) +def echo(msg): + print msg + +s.on('get_player_info:d.camata@gmail.com', echo) +s.emit('remote:send_request', {'account': 'd.camata@gmail.com', 'name': 'get_player_info'}) +s.wait(1) diff --git a/pokemongo_bot/websocket_remote_control.py b/pokemongo_bot/websocket_remote_control.py index 023db22ff4..cd5bd5af96 100644 --- a/pokemongo_bot/websocket_remote_control.py +++ b/pokemongo_bot/websocket_remote_control.py @@ -46,8 +46,8 @@ def get_player_info(self): request.get_player() request.get_inventory() response_dict = request.call() - inventory = response_dict['responses']['GET_INVENTORY'] - player_info = response_dict['responses']['GET_PLAYER'] + inventory = response_dict['responses'].get('GET_INVENTORY', {}) + player_info = response_dict['responses'].get('GET_PLAYER', {}) self.sio.emit( 'bot:send_reply', { From 61df52fb4d3b517620fcb48be20345ecadc7e295 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Wed, 10 Aug 2016 02:13:55 +0200 Subject: [PATCH 073/143] keeping the account in the remote command response --- pokemongo_bot/socketio_server/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/socketio_server/app.py b/pokemongo_bot/socketio_server/app.py index 7f3c5040d2..a970a30479 100644 --- a/pokemongo_bot/socketio_server/app.py +++ b/pokemongo_bot/socketio_server/app.py @@ -20,7 +20,7 @@ def remote_control(sid, command): @sio.on('bot:send_reply') def request_reply(sid, response): event = response.pop('command') - account = response.pop('account') + account = response['account'] event = "{}:{}".format(event, account) sio.emit(event, response) From 9f9146c0f9e418c3ab07ab177983c556651b7ed6 Mon Sep 17 00:00:00 2001 From: Eevee Date: Wed, 10 Aug 2016 09:15:04 +0900 Subject: [PATCH 074/143] Add ColoredLoggingHandler (#3198) --- CONTRIBUTORS.md | 1 + configs/config.json.cluster.example | 1 + configs/config.json.example | 1 + configs/config.json.map.example | 1 + configs/config.json.path.example | 1 + configs/config.json.pokemon.example | 1 + pokecli.py | 8 + pokemongo_bot/__init__.py | 10 +- pokemongo_bot/event_handlers/__init__.py | 1 + .../event_handlers/colored_logging_handler.py | 172 ++++++++++++++++++ 10 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 pokemongo_bot/event_handlers/colored_logging_handler.py diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 57b289ab90..bb0ac15b7b 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -59,3 +59,4 @@ * nikhil-pandey * thebigjc * JaapMoolenaar + * eevee-github diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index e4597519f6..eb507cd43c 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -81,6 +81,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.example b/configs/config.json.example index d2e8d4f064..08c844218c 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -89,6 +89,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.map.example b/configs/config.json.map.example index 56a006450c..cf6604976b 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -323,6 +323,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 6f7b04c305..6b6619573b 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -83,6 +83,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index e7ba38dc37..1dfa01199d 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -89,6 +89,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or" }, diff --git a/pokecli.py b/pokecli.py index 6811da7696..f59ae3acb9 100644 --- a/pokecli.py +++ b/pokecli.py @@ -395,6 +395,14 @@ def _json_loader(filename): type=float, default=5.0 ) + add_config( + parser, + load, + long_flag="--logging_color", + help="If logging_color is set to true, colorized logging handler will be used", + type=bool, + default=True + ) # Start to parse other attrs config = parser.parse_args() diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 133bb4631e..31e7eb0bd1 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -25,7 +25,7 @@ from human_behaviour import sleep from item_list import Item from metrics import Metrics -from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler +from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler, ColoredLoggingHandler from pokemongo_bot.socketio_server.runner import SocketIoRunner from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl from pokemongo_bot.base_dir import _base_dir @@ -88,7 +88,12 @@ def start(self): random.seed() def _setup_event_system(self): - handlers = [LoggingHandler()] + handlers = [] + if self.config.logging_color: + handlers.append(ColoredLoggingHandler()) + else: + handlers.append(LoggingHandler()) + if self.config.websocket_server_url: if self.config.websocket_start_embedded_server: self.sio_runner = SocketIoRunner(self.config.websocket_server_url) @@ -103,7 +108,6 @@ def _setup_event_system(self): if self.config.websocket_remote_control: remote_control = WebsocketRemoteControl(self).start() - self.event_manager = EventManager(*handlers) self._register_events() if self.config.show_events: diff --git a/pokemongo_bot/event_handlers/__init__.py b/pokemongo_bot/event_handlers/__init__.py index f0933a0e68..a4dd6fce7f 100644 --- a/pokemongo_bot/event_handlers/__init__.py +++ b/pokemongo_bot/event_handlers/__init__.py @@ -1,2 +1,3 @@ from logging_handler import LoggingHandler from socketio_handler import SocketIoHandler +from colored_logging_handler import ColoredLoggingHandler diff --git a/pokemongo_bot/event_handlers/colored_logging_handler.py b/pokemongo_bot/event_handlers/colored_logging_handler.py new file mode 100644 index 0000000000..80e5414ae6 --- /dev/null +++ b/pokemongo_bot/event_handlers/colored_logging_handler.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import time +import sys +import struct + +from pokemongo_bot.event_manager import EventHandler + +class ColoredLoggingHandler(EventHandler): + EVENT_COLOR_MAP = { + 'api_error': 'red', + 'bot_exit': 'red', + 'bot_start': 'green', + 'config_error': 'red', + 'egg_already_incubating': 'yellow', + 'egg_hatched': 'green', + 'future_pokemon_release': 'yellow', + 'incubate': 'green', + 'incubator_already_used': 'yellow', + 'inventory_full': 'yellow', + 'item_discard_fail': 'red', + 'item_discarded': 'green', + 'keep_best_release': 'green', + 'level_up': 'green', + 'level_up_reward': 'green', + 'location_cache_error': 'yellow', + 'location_cache_ignored': 'yellow', + 'login_failed': 'red', + 'login_successful': 'green', + 'lucky_egg_error': 'red', + 'move_to_map_pokemon_encounter': 'green', + 'move_to_map_pokemon_fail': 'red', + 'next_egg_incubates': 'yellow', + 'next_sleep': 'green', + 'no_pokeballs': 'red', + 'pokemon_appeared': 'yellow', + 'pokemon_capture_failed': 'red', + 'pokemon_caught': 'blue', + 'pokemon_evolved': 'green', + 'pokemon_fled': 'red', + 'pokemon_inventory_full': 'red', + 'pokemon_nickname_invalid': 'red', + 'pokemon_not_in_range': 'yellow', + 'pokemon_release': 'green', + 'pokemon_vanished': 'red', + 'pokestop_empty': 'yellow', + 'pokestop_searching_too_often': 'yellow', + 'rename_pokemon': 'green', + 'skip_evolve': 'yellow', + 'softban': 'red', + 'spun_pokestop': 'cyan', + 'threw_berry_failed': 'red', + 'unknown_spin_result': 'red', + 'unset_pokemon_nickname': 'red', + 'vip_pokemon': 'red', + + # event names for 'white' still here to remember that these events are already determined its color. + 'arrived_at_cluster': 'white', + 'arrived_at_fort': 'white', + 'bot_sleep': 'white', + 'catchable_pokemon': 'white', + 'found_cluster': 'white', + 'incubate_try': 'white', + 'load_cached_location': 'white', + 'location_found': 'white', + 'login_started': 'white', + 'lured_pokemon_found': 'white', + 'move_to_map_pokemon_move_towards': 'white', + 'move_to_map_pokemon_teleport_back': 'white', + 'move_to_map_pokemon_updated_map': 'white', + 'moving_to_fort': 'white', + 'moving_to_lured_fort': 'white', + 'pokemon_catch_rate': 'white', + 'pokemon_evolve_fail': 'white', + 'pokestop_on_cooldown': 'white', + 'pokestop_out_of_range': 'white', + 'polyline_request': 'white', + 'position_update': 'white', + 'set_start_location': 'white', + 'softban_fix': 'white', + 'softban_fix_done': 'white', + 'spun_fort': 'white', + 'threw_berry': 'white', + 'threw_pokeball': 'white', + 'used_lucky_egg': 'white' + } + CONTINUOUS_EVENT_NAMES = [ + 'catchable_pokemon', + 'moving_to_lured_fort', + 'spun_fort' + ] + COLOR_CODE = { + 'red': '91', + 'green': '92', + 'yellow': '93', + 'blue': '94', + 'cyan': '96' + } + + def __init__(self): + self._last_event = None + try: + # this `try ... except` is for ImportError on Windows + import fcntl + import termios + self._ioctl = fcntl.ioctl + self._TIOCGWINSZ = termios.TIOCGWINSZ + except ImportError: + self._ioctl = None + self._TIOCGWINSZ = None + + def handle_event(self, event, sender, level, formatted_msg, data): + # Prepare message string + message = None + if formatted_msg: + try: + message = formatted_msg.decode('utf-8') + except UnicodeEncodeError: + message = formatted_msg + else: + message = '{}'.format(str(data)) + + # Replace message if necessary + if event == 'catchable_pokemon': + message = 'Something rustles nearby!' + + # Truncate previous line if same event continues + if event in ColoredLoggingHandler.CONTINUOUS_EVENT_NAMES and self._last_event == event: + # Filling with "' ' * terminal_width" in order to completely clear last line + terminal_width = self._terminal_width() + if terminal_width: + sys.stdout.write('\r{}\r'.format(' ' * terminal_width)) + else: + sys.stdout.write('\r') + else: + sys.stdout.write("\n") + + color_name = None + if event in ColoredLoggingHandler.EVENT_COLOR_MAP: + color_name = ColoredLoggingHandler.EVENT_COLOR_MAP[event] + + # Change color if necessary + if event == 'egg_hatched' and data.get('pokemon', 'error') == 'error': + # `egg_hatched` event will be dispatched in both cases: hatched pokemon info is successfully taken or not. + # change color from 'green' to 'red' in case of error. + color_name = 'red' + + if color_name in ColoredLoggingHandler.COLOR_CODE: + sys.stdout.write( + '[{time}] \033[{color}m{message}\033[0m'.format( + time=time.strftime("%H:%M:%S"), + color=ColoredLoggingHandler.COLOR_CODE[color_name], + message=message + ) + ) + else: + sys.stdout.write('[{time}] {message}'.format( + time=time.strftime("%H:%M:%S"), + message=message + )) + + sys.stdout.flush() + self._last_event = event + + def _terminal_width(self): + if self._ioctl is None or self._TIOCGWINSZ is None: + return None + + h, w, hp, wp = struct.unpack('HHHH', + self._ioctl(0, self._TIOCGWINSZ, + struct.pack('HHHH', 0, 0, 0, 0))) + return w From 8b5af8d766ac161be926932b05e3fc785a33661d Mon Sep 17 00:00:00 2001 From: achretien Date: Wed, 10 Aug 2016 02:17:46 +0200 Subject: [PATCH 075/143] Update TransferPokemon to use new Inventory Class (#3320) * Update TransferPokemon to use new Inventory Class * Use base_dir * Don't release pokemon if bot is on test mode --- .../cell_workers/transfer_pokemon.py | 202 +++++++----------- pokemongo_bot/inventory.py | 15 +- 2 files changed, 91 insertions(+), 126 deletions(-) diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 88548e75b2..6b3eebac39 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -1,9 +1,10 @@ import json import os -from pokemongo_bot.base_dir import _base_dir +from pokemongo_bot import inventory from pokemongo_bot.human_behaviour import action_delay from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.inventory import Pokemons class TransferPokemon(BaseTask): @@ -11,129 +12,75 @@ class TransferPokemon(BaseTask): def work(self): pokemon_groups = self._release_pokemon_get_groups() - for pokemon_id in pokemon_groups: - group = pokemon_groups[pokemon_id] - - if len(group) > 0: - pokemon_name = self.bot.pokemon_list[pokemon_id - 1]['Name'] - keep_best, keep_best_cp, keep_best_iv = self._validate_keep_best_config(pokemon_name) - - if keep_best: - best_pokemon_ids = set() - order_criteria = 'none' - if keep_best_cp >= 1: - cp_limit = keep_best_cp - best_cp_pokemons = sorted(group, key=lambda x: (x['cp'], x['iv']), reverse=True)[:cp_limit] - best_pokemon_ids = set(pokemon['pokemon_data']['id'] for pokemon in best_cp_pokemons) - order_criteria = 'cp' - - if keep_best_iv >= 1: - iv_limit = keep_best_iv - best_iv_pokemons = sorted(group, key=lambda x: (x['iv'], x['cp']), reverse=True)[:iv_limit] - best_pokemon_ids |= set(pokemon['pokemon_data']['id'] for pokemon in best_iv_pokemons) - if order_criteria == 'cp': - order_criteria = 'cp and iv' - else: - order_criteria = 'iv' - - # remove best pokemons from all pokemons array - all_pokemons = group - best_pokemons = [] - for best_pokemon_id in best_pokemon_ids: - for pokemon in all_pokemons: - if best_pokemon_id == pokemon['pokemon_data']['id']: - all_pokemons.remove(pokemon) - best_pokemons.append(pokemon) - - transfer_pokemons = [pokemon for pokemon in all_pokemons - if self.should_release_pokemon(pokemon_name, - pokemon['cp'], - pokemon['iv'], - True)] - - if transfer_pokemons: - if best_pokemons: - self.emit_event( - 'keep_best_release', - formatted="Keeping best {amount} {pokemon}, based on {criteria}", - data={ - 'amount': len(best_pokemons), - 'pokemon': pokemon_name, - 'criteria': order_criteria - } - ) - for pokemon in transfer_pokemons: - self.release_pokemon(pokemon_name, pokemon['cp'], pokemon['iv'], pokemon['pokemon_data']['id']) - else: - group = sorted(group, key=lambda x: x['cp'], reverse=True) - for item in group: - pokemon_cp = item['cp'] - pokemon_potential = item['iv'] - - if self.should_release_pokemon(pokemon_name, pokemon_cp, pokemon_potential): - self.release_pokemon(pokemon_name, item['cp'], item['iv'], item['pokemon_data']['id']) + for pokemon_id, group in pokemon_groups.iteritems(): + pokemon_name = Pokemons.name_for(pokemon_id) + keep_best, keep_best_cp, keep_best_iv = self._validate_keep_best_config(pokemon_name) + + if keep_best: + best_pokemon_ids = set() + order_criteria = 'none' + if keep_best_cp >= 1: + cp_limit = keep_best_cp + best_cp_pokemons = sorted(group, key=lambda x: (x.cp, x.iv), reverse=True)[:cp_limit] + best_pokemon_ids = set(pokemon.id for pokemon in best_cp_pokemons) + order_criteria = 'cp' + + if keep_best_iv >= 1: + iv_limit = keep_best_iv + best_iv_pokemons = sorted(group, key=lambda x: (x.iv, x.cp), reverse=True)[:iv_limit] + best_pokemon_ids |= set(pokemon.id for pokemon in best_iv_pokemons) + if order_criteria == 'cp': + order_criteria = 'cp and iv' + else: + order_criteria = 'iv' + + # remove best pokemons from all pokemons array + all_pokemons = group + best_pokemons = [] + for best_pokemon_id in best_pokemon_ids: + for pokemon in all_pokemons: + if best_pokemon_id == pokemon.id: + all_pokemons.remove(pokemon) + best_pokemons.append(pokemon) + + transfer_pokemons = [pokemon for pokemon in all_pokemons if self.should_release_pokemon(pokemon,True)] + + if transfer_pokemons: + if best_pokemons: + self.emit_event( + 'keep_best_release', + formatted="Keeping best {amount} {pokemon}, based on {criteria}", + data={ + 'amount': len(best_pokemons), + 'pokemon': pokemon_name, + 'criteria': order_criteria + } + ) + for pokemon in transfer_pokemons: + self.release_pokemon(pokemon) + else: + group = sorted(group, key=lambda x: x.cp, reverse=True) + for pokemon in group: + if self.should_release_pokemon(pokemon): + self.release_pokemon(pokemon) def _release_pokemon_get_groups(self): pokemon_groups = {} - request = self.bot.api.create_request() - request.get_player() - request.get_inventory() - inventory_req = request.call() - - if inventory_req.get('responses', False) is False: - return pokemon_groups - - inventory_dict = inventory_req['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] - - user_web_inventory = os.path.join(_base_dir, 'web', 'inventory-%s.json' % (self.bot.config.username)) - with open(user_web_inventory, 'w') as outfile: - json.dump(inventory_dict, outfile) - - for pokemon in inventory_dict: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data", "pokemon_id" - ], pokemon) - except KeyError: - continue - - pokemon_data = pokemon['inventory_item_data']['pokemon_data'] - - # pokemon in fort, so we cant transfer it - if 'deployed_fort_id' in pokemon_data and pokemon_data['deployed_fort_id']: - continue - - # favorite pokemon can't transfer in official game client - if pokemon_data.get('favorite', 0) is 1: + for pokemon in inventory.pokemons(True).all(): + if pokemon.in_fort or pokemon.is_favorite: continue - group_id = pokemon_data['pokemon_id'] - group_pokemon_cp = pokemon_data['cp'] - group_pokemon_iv = self.get_pokemon_potential(pokemon_data) + group_id = pokemon.pokemon_id if group_id not in pokemon_groups: pokemon_groups[group_id] = [] - pokemon_groups[group_id].append({ - 'cp': group_pokemon_cp, - 'iv': group_pokemon_iv, - 'pokemon_data': pokemon_data - }) + pokemon_groups[group_id].append(pokemon) return pokemon_groups - def get_pokemon_potential(self, pokemon_data): - total_iv = 0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - for individual_stat in iv_stats: - try: - total_iv += pokemon_data[individual_stat] - except Exception: - continue - return round((total_iv / 45.0), 2) - - def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): - release_config = self._get_release_config_for(pokemon_name) + def should_release_pokemon(self, pokemon, keep_best_mode = False): + release_config = self._get_release_config_for(pokemon.name) if (keep_best_mode and not release_config.has_key('never_release') @@ -158,11 +105,11 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): return True release_cp = release_config.get('release_below_cp', 0) - if cp < release_cp: + if pokemon.cp < release_cp: release_results['cp'] = True release_iv = release_config.get('release_below_iv', 0) - if iv < release_iv: + if pokemon.iv < release_iv: release_results['iv'] = True logic_to_function = { @@ -175,9 +122,9 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): 'future_pokemon_release', formatted="Releasing {pokemon} (CP {cp}/IV {iv}) based on rule: CP < {below_cp} {cp_iv_logic} IV < {below_iv}", data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': iv, + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv, 'below_cp': release_cp, 'cp_iv_logic': cp_iv_logic.upper(), 'below_iv': release_iv @@ -186,16 +133,27 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): return logic_to_function[cp_iv_logic](*release_results.values()) - def release_pokemon(self, pokemon_name, cp, iv, pokemon_id): - response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon_id) + def release_pokemon(self, pokemon): + try: + if self.bot.config.test: + candy_awarded = 1 + else: + response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon.id) + candy_awarded = response_dict['responses']['RELEASE_POKEMON']['candy_awarded'] + except KeyError: + return + + # We could refresh here too, but adding 1 saves a inventory request + candy = inventory.candies().get(pokemon.pokemon_id) + candy.add(candy_awarded) self.bot.metrics.released_pokemon() self.emit_event( 'pokemon_release', formatted='Exchanged {pokemon} [CP {cp}] [IV {iv}] for candy.', data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': iv + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv } ) action_delay(self.bot.config.action_wait_min, self.bot.config.action_wait_max) diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index 1fc208d235..ea81b7c093 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -164,9 +164,11 @@ def __init__(self, data): self._static_data = Pokemons.data_for(self.pokemon_id) self.name = Pokemons.name_for(self.pokemon_id) self.iv = self._compute_iv() + self.in_fort = 'deployed_fort_id' in data + self.is_favorite = data.get('favorite', 0) is 1 def can_evolve_now(self): - return self.has_next_evolution() and self.candy_quantity > self.evolution_cost + return self.has_next_evolution() and self.candy_quantity >= self.evolution_cost def has_next_evolution(self): return 'Next Evolution Requirements' in self._static_data @@ -217,11 +219,14 @@ def refresh(self): # TODO: it would be better if this class was used for all # inventory management. For now, I'm just clearing the old inventory field self.bot.latest_inventory = None - inventory = self.bot.get_inventory()['responses']['GET_INVENTORY'][ - 'inventory_delta']['inventory_items'] + inventory = self.bot.get_inventory()['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] for i in (self.pokedex, self.candy, self.items, self.pokemons): i.refresh(inventory) + user_web_inventory = os.path.join(_base_dir, 'web', 'inventory-%s.json' % (self.bot.config.username)) + with open(user_web_inventory, 'w') as outfile: + json.dump(inventory, outfile) + _inventory = None @@ -244,7 +249,9 @@ def candies(refresh=False): return _inventory.candy -def pokemons(): +def pokemons(refresh=False): + if refresh: + refresh_inventory() return _inventory.pokemons From 310f5786049318050f424358c144829f5d995e37 Mon Sep 17 00:00:00 2001 From: Simon Shi Date: Wed, 10 Aug 2016 12:53:30 +0800 Subject: [PATCH 076/143] Some text fixes for setup.sh (#3390) Minor text fix. --- setup.sh | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/setup.sh b/setup.sh index bcff0feefb..07589917e9 100755 --- a/setup.sh +++ b/setup.sh @@ -37,7 +37,7 @@ Input location " location read -p "Input gmapkey " gmapkey -cp configs/config.json.example configs/config.json +cp -f configs/config.json.example configs/config.json if [ "$auth" = "2" ] then sed -i "s/google/ptc/g" configs/config.json @@ -46,7 +46,7 @@ sed -i "s/YOUR_USERNAME/$username/g" configs/config.json sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json sed -i "s/SOME_LOCATION/$location/g" configs/config.json sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json -echo "Edit configs/config.json to modify any other config." +echo "Edit ./configs/config.json to modify any other config." } function Pokebotinstall () { @@ -67,12 +67,14 @@ echo "You are on Mac os" sudo brew update sudo brew install --devel protobuf else -echo "Nothing happend." +echo "Please check if you have python pip protobuf gcc make installed on your device." +echo "Wait 5 seconds to continue or Use ctrl+c to interrupt this shell." +sleep 5 fi sudo pip install virtualenv Pokebotupdate Pokebotencrypt -echo "Install complete." +echo "Install complete. Starting to generate config.json." Pokebotconfig } @@ -89,7 +91,7 @@ echo " -i,--install. Install PokemonGo-Bot." echo " -b,--backup. Backup config files." echo " -c,--config. Easy config generator." echo " -e,--encrypt. Make encrypt.so." -echo " -r,--reset. Force sync dev branch." +echo " -r,--reset. Force sync dev branch." echo " -u,--update. Command git pull to update." } @@ -120,12 +122,13 @@ Pokebothelp ;; *.json) filename=$* +echo "It's better to use run.sh, not this one." cd $pokebotpath if [ ! -f ./configs/"$filename" ] then -echo "There's no ./configs/"$filename" file. It's better to use run.sh not this one." +echo "There's no ./configs/"$filename" file. It's better to use run.sh, not this one." else -Pokebotrun +./run ./configs/"$filename" fi ;; *) From b3f9e0f2951e55342a8f4a2036c333c2189571c3 Mon Sep 17 00:00:00 2001 From: Simon Shi Date: Wed, 10 Aug 2016 13:40:39 +0800 Subject: [PATCH 077/143] Fix path of shells in install.sh (#3393) * Update install.sh Fix path * Update setup.sh fix path * Update run.sh fix path --- install.sh | 10 ++++++---- run.sh | 2 +- setup.sh | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/install.sh b/install.sh index 32cc1e1124..1e7415ed30 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -pokebotpath=$(pwd) +pokebotpath=$(cd "$(dirname "$0")"; pwd) cd $pokebotpath if [ -f /etc/debian_version ] then @@ -17,7 +17,9 @@ echo "You are on Mac os" sudo brew update sudo brew install --devel protobuf else -echo "Nothing happend." +echo "Please check if you have python pip protobuf gcc make installed on your device." +echo "Wait 5 seconds to continue or Use ctrl+c to interrupt this shell." +sleep 5 fi pip install virtualenv cd $pokebotpath @@ -36,7 +38,7 @@ mv libencrypt.so $pokebotpath/encrypt.so cd ../.. rm -rf pgoencrypt.tar.gz rm -rf pgoencrypt -echo "Install complete." +echo "Install complete. Starting to generate config.json." cd $pokebotpath read -p "1.google 2.ptc " auth @@ -58,5 +60,5 @@ sed -i "s/YOUR_USERNAME/$username/g" configs/config.json sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json sed -i "s/SOME_LOCATION/$location/g" configs/config.json sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json -echo "Edit configs/config.json to modify any other config. Use run.sh to run." +echo "Edit configs/config.json to modify any other config. Use run.sh ./configs/config.json to run." exit 0 diff --git a/run.sh b/run.sh index d8406697e7..4c93cda234 100755 --- a/run.sh +++ b/run.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -pokebotpath=$(pwd) +pokebotpath=$(cd "$(dirname "$0")"; pwd) filename="" if [ ! -z $1 ]; then filename=$1 diff --git a/setup.sh b/setup.sh index 07589917e9..84baef623a 100755 --- a/setup.sh +++ b/setup.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -pokebotpath=$(pwd) -backuppath=$(pwd)"/backup" +pokebotpath=$(cd "$(dirname "$0")"; pwd) +backuppath=$pokebotpath"/backup" function Pokebotupdate () { cd $pokebotpath From 67fe00bcfc04fb6ddd2c78ca208aad4e4cbf2523 Mon Sep 17 00:00:00 2001 From: jebabin Date: Wed, 10 Aug 2016 07:42:14 +0200 Subject: [PATCH 078/143] Fix evolution error in pokemon.json (#3344) Fix evolution error in pokemon.json --- data/pokemon.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/data/pokemon.json b/data/pokemon.json index 58f7c437f2..e64b7c31fb 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -1600,8 +1600,8 @@ }, "Next evolution(s)": [ { - "Number": "039", - "Name": "Jigglypuff" + "Number": "040", + "Name": "Wigglytuff" } ], "Special Attack(s)": [ @@ -1633,8 +1633,8 @@ "Height": "1.0 m", "Previous evolution(s)": [ { - "Number": "040", - "Name": "Wigglytuff" + "Number": "039", + "Name": "Jigglypuff" } ], "Special Attack(s)": [ @@ -4820,8 +4820,8 @@ }, "Next evolution(s)": [ { - "Number": "120", - "Name": "Staryu" + "Number": "121", + "Name": "Starmie" } ], "Special Attack(s)": [ @@ -4860,8 +4860,8 @@ "Height": "1.1 m", "Previous evolution(s)": [ { - "Number": "121", - "Name": "Starmie" + "Number": "120", + "Name": "Staryu" } ], "Special Attack(s)": [ From 01bc14de0543196de6688dc6bd560739db59fd14 Mon Sep 17 00:00:00 2001 From: Sean Quinn Date: Wed, 10 Aug 2016 03:18:09 -0400 Subject: [PATCH 079/143] Improve formatting consistency in transfer_pokemon.py (#3397) Improve formatting consistency --- pokemongo_bot/cell_workers/transfer_pokemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 6b3eebac39..9e970d7d7f 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -120,7 +120,7 @@ def should_release_pokemon(self, pokemon, keep_best_mode = False): if logic_to_function[cp_iv_logic](*release_results.values()): self.emit_event( 'future_pokemon_release', - formatted="Releasing {pokemon} (CP {cp}/IV {iv}) based on rule: CP < {below_cp} {cp_iv_logic} IV < {below_iv}", + formatted="Releasing {pokemon} [CP {cp}] [IV {iv}] based on rule: CP < {below_cp} {cp_iv_logic} IV < {below_iv}", data={ 'pokemon': pokemon.name, 'cp': pokemon.cp, From d22c5b2c4b9b0602819d50dc03496aafd1f628e6 Mon Sep 17 00:00:00 2001 From: brantje Date: Wed, 10 Aug 2016 11:47:27 +0200 Subject: [PATCH 080/143] Remove unnecessary file --- brantje.py | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 brantje.py diff --git a/brantje.py b/brantje.py deleted file mode 100644 index 0674ceb077..0000000000 --- a/brantje.py +++ /dev/null @@ -1,9 +0,0 @@ -# coding: utf-8 -from socketIO_client import SocketIO -s = SocketIO('localhost', 4000) -def echo(msg): - print msg - -s.on('get_player_info:d.camata@gmail.com', echo) -s.emit('remote:send_request', {'account': 'd.camata@gmail.com', 'name': 'get_player_info'}) -s.wait(1) From 0c3c4c004d5081b91ffe278850d1b1821e4f63b4 Mon Sep 17 00:00:00 2001 From: Simon Shi Date: Wed, 10 Aug 2016 17:58:03 +0800 Subject: [PATCH 081/143] Put info on the next line in run.sh (#3422) * Update setup.sh fix typo * Update run.sh fix typo --- run.sh | 5 +++-- setup.sh | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/run.sh b/run.sh index 4c93cda234..becbf01f05 100755 --- a/run.sh +++ b/run.sh @@ -15,7 +15,8 @@ while true do cd $pokebotpath python pokecli.py -cf $filename -read -p "Press any button or wait 20 seconds." -r -s -n1 -t 20 -echo `date`"Pokebot"$*" Stopped." +read -p "Press any button or wait 20 seconds to continue. +" -r -s -n1 -t 20 +echo `date`" Pokebot"$*" Stopped." done exit 0 diff --git a/setup.sh b/setup.sh index 84baef623a..fef667bd2d 100755 --- a/setup.sh +++ b/setup.sh @@ -128,7 +128,7 @@ if [ ! -f ./configs/"$filename" ] then echo "There's no ./configs/"$filename" file. It's better to use run.sh, not this one." else -./run ./configs/"$filename" +./run.sh ./configs/"$filename" fi ;; *) From 72622b438ebd65283ee14d136e2ee610612c43f6 Mon Sep 17 00:00:00 2001 From: vanishing Date: Wed, 10 Aug 2016 08:37:33 -0400 Subject: [PATCH 082/143] Fix Struct() argument 1 must be string, not unicode. (#3375) --- CONTRIBUTORS.md | 1 + pokemongo_bot/event_handlers/colored_logging_handler.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index bb0ac15b7b..a963396fcb 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -60,3 +60,4 @@ * thebigjc * JaapMoolenaar * eevee-github + * g0vanish diff --git a/pokemongo_bot/event_handlers/colored_logging_handler.py b/pokemongo_bot/event_handlers/colored_logging_handler.py index 80e5414ae6..dd7239d6d6 100644 --- a/pokemongo_bot/event_handlers/colored_logging_handler.py +++ b/pokemongo_bot/event_handlers/colored_logging_handler.py @@ -166,7 +166,7 @@ def _terminal_width(self): if self._ioctl is None or self._TIOCGWINSZ is None: return None - h, w, hp, wp = struct.unpack('HHHH', + h, w, hp, wp = struct.unpack(str('HHHH'), self._ioctl(0, self._TIOCGWINSZ, - struct.pack('HHHH', 0, 0, 0, 0))) + struct.pack(str('HHHH'), 0, 0, 0, 0))) return w From 558540ed1a9723345f1a25b4f363b441424cbc30 Mon Sep 17 00:00:00 2001 From: achretien Date: Wed, 10 Aug 2016 19:03:04 +0200 Subject: [PATCH 083/143] Give the possibility to disable a task without removing it (#3417) * Give the possiblity to disable a task in config without removing it from the config file * Put exmple only in nickname task * Add Unit testing * typo * Use enabled false as exemple --- configs/config.json.example | 7 +++++++ pokemongo_bot/base_task.py | 1 + pokemongo_bot/tree_config_builder.py | 3 ++- tests/tree_config_builder_test.py | 19 +++++++++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/configs/config.json.example b/configs/config.json.example index 08c844218c..59974ba156 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -21,6 +21,13 @@ { "type": "TransferPokemon" }, + { + "type": "NicknamePokemon", + "config": { + "enabled": false, + "nickname_template": "{iv_pct}_{iv_ads}" + } + }, { "type": "EvolvePokemon", "config": { diff --git a/pokemongo_bot/base_task.py b/pokemongo_bot/base_task.py index 22bbedf4e8..1b610d31aa 100644 --- a/pokemongo_bot/base_task.py +++ b/pokemongo_bot/base_task.py @@ -9,6 +9,7 @@ def __init__(self, bot, config): self.config = config self._validate_work_exists() self.logger = logging.getLogger(type(self).__name__) + self.enabled = config.get('enabled', True) self.initialize() def _validate_work_exists(self): diff --git a/pokemongo_bot/tree_config_builder.py b/pokemongo_bot/tree_config_builder.py index 6242747b25..57dc9da33c 100644 --- a/pokemongo_bot/tree_config_builder.py +++ b/pokemongo_bot/tree_config_builder.py @@ -61,7 +61,8 @@ def build(self): ) instance = worker(self.bot, task_config) - workers.append(instance) + if instance.enabled: + workers.append(instance) return workers diff --git a/tests/tree_config_builder_test.py b/tests/tree_config_builder_test.py index 1992c8187c..f8982cbfad 100644 --- a/tests/tree_config_builder_test.py +++ b/tests/tree_config_builder_test.py @@ -86,6 +86,25 @@ def test_task_with_config(self): tree = builder.build() self.assertTrue(tree[0].config.get('longer_eggs_first', False)) + def test_disabling_task(self): + obj = convert_from_json("""[{ + "type": "HandleSoftBan", + "config": { + "enabled": false + } + }, { + "type": "CatchLuredPokemon", + "config": { + "enabled": true + } + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + tree = builder.build() + + self.assertTrue(len(tree) == 1) + self.assertIsInstance(tree[0], CatchLuredPokemon) + def test_load_plugin_task(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') plugin_loader = PluginLoader() From 99187abd7d4407ae57a69e4f0ad9126d60f70236 Mon Sep 17 00:00:00 2001 From: nivong Date: Wed, 10 Aug 2016 19:25:13 +0200 Subject: [PATCH 084/143] fix config creation (#3482) Changed auth to be more specifik and added right permissions. --- setup.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.sh b/setup.sh index fef667bd2d..5fce474b21 100755 --- a/setup.sh +++ b/setup.sh @@ -26,7 +26,7 @@ rm -rf pgoencrypt function Pokebotconfig () { cd $pokebotpath -read -p "1.google 2.ptc +read -p "enter 1 for google or 2 for ptc " auth read -p "Input username " username @@ -37,7 +37,7 @@ Input location " location read -p "Input gmapkey " gmapkey -cp -f configs/config.json.example configs/config.json +cp -f configs/config.json.example configs/config.json && chmod 755 if [ "$auth" = "2" ] then sed -i "s/google/ptc/g" configs/config.json From edeb2c23f3bcae117dab99b1f547a351bc43af8b Mon Sep 17 00:00:00 2001 From: leadboots5 Date: Wed, 10 Aug 2016 12:56:52 -0500 Subject: [PATCH 085/143] Remove unused IV calculation from evolve_pokemon (#3487) Previously IV was computed in each worker. Now its fetched from inventory. This was left over and not called in the worker at all. --- pokemongo_bot/cell_workers/evolve_pokemon.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 4cc451115b..7380f1c5db 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -117,8 +117,3 @@ def _execute_pokemon_evolve(self, pokemon, cache): cache[pokemon.name] = 1 sleep(0.7) return False - - def _compute_iv(self, pokemon): - total_iv = pokemon.get("individual_attack", 0) + pokemon.get("individual_stamina", 0) + pokemon.get( - "individual_defense", 0) - return round((total_iv / 45.0), 2) From f823d77b091b8f4bb047afabfec745a24af51d13 Mon Sep 17 00:00:00 2001 From: achretien Date: Wed, 10 Aug 2016 20:05:10 +0200 Subject: [PATCH 086/143] Don't show Inventory full event if we set "ignore_item_count" (#3440) --- pokemongo_bot/cell_workers/move_to_fort.py | 2 +- pokemongo_bot/cell_workers/spin_fort.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 7dcd0977b1..2be287e86c 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -19,7 +19,7 @@ def initialize(self): def should_run(self): has_space_for_loot = self.bot.has_space_for_loot() - if not has_space_for_loot: + if not has_space_for_loot and not self.ignore_item_count: self.emit_event( 'inventory_full', formatted="Inventory is full. You might want to change your config to recycle more items if this message appears consistently." diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 445946e7e1..9422d8ec35 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -19,7 +19,7 @@ def initialize(self): self.ignore_item_count = self.config.get("ignore_item_count", False) def should_run(self): - if not self.bot.has_space_for_loot(): + if not self.bot.has_space_for_loot() and not self.ignore_item_count: self.emit_event( 'inventory_full', formatted="Inventory is full. You might want to change your config to recycle more items if this message appears consistently." @@ -106,10 +106,11 @@ def work(self): data={'pokestop': fort_name, 'minutes_left': minutes_left} ) elif spin_result == 4: - self.emit_event( - 'inventory_full', - formatted="Inventory is full!" - ) + if not self.ignore_item_count: + self.emit_event( + 'inventory_full', + formatted="Inventory is full!" + ) else: self.emit_event( 'unknown_spin_result', From 9ae1b785f0836d8769d011626d2bc071ed338bff Mon Sep 17 00:00:00 2001 From: Simon Shi Date: Thu, 11 Aug 2016 02:11:34 +0800 Subject: [PATCH 087/143] Fix showing the date in run.sh (#3433) fix the logic of showing the date --- run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.sh b/run.sh index becbf01f05..9938f8c5e0 100755 --- a/run.sh +++ b/run.sh @@ -15,8 +15,8 @@ while true do cd $pokebotpath python pokecli.py -cf $filename +echo `date`" Pokebot "$*" Stopped." read -p "Press any button or wait 20 seconds to continue. " -r -s -n1 -t 20 -echo `date`" Pokebot"$*" Stopped." done exit 0 From 1183b934708cb98cb7e05dc12d2c30a034dc0229 Mon Sep 17 00:00:00 2001 From: Dmitry Ovodov Date: Thu, 11 Aug 2016 05:35:16 +0700 Subject: [PATCH 088/143] Typo fix: show new catch rate after berry throw. (#3521) --- pokemongo_bot/cell_workers/pokemon_catch_worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 345fcabc7e..3b2092e535 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -251,7 +251,7 @@ def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, cu data={ 'berry_name': self.item_list[str(berry_id)], 'ball_name': self.item_list[str(current_ball)], - 'new_catch_rate': self._pct(catch_rate_by_ball[current_ball]) + 'new_catch_rate': self._pct(new_catch_rate_by_ball[current_ball]) } ) From 41e5758961d7308f68aed4bad137f1201e5ed209 Mon Sep 17 00:00:00 2001 From: net8q Date: Thu, 11 Aug 2016 00:36:46 +0200 Subject: [PATCH 089/143] Fix stdout is not a terminal (#3511) --- pokemongo_bot/event_handlers/colored_logging_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/event_handlers/colored_logging_handler.py b/pokemongo_bot/event_handlers/colored_logging_handler.py index dd7239d6d6..f3c902d464 100644 --- a/pokemongo_bot/event_handlers/colored_logging_handler.py +++ b/pokemongo_bot/event_handlers/colored_logging_handler.py @@ -125,7 +125,7 @@ def handle_event(self, event, sender, level, formatted_msg, data): message = 'Something rustles nearby!' # Truncate previous line if same event continues - if event in ColoredLoggingHandler.CONTINUOUS_EVENT_NAMES and self._last_event == event: + if event in ColoredLoggingHandler.CONTINUOUS_EVENT_NAMES and self._last_event == event and sys.stdout.isatty(): # Filling with "' ' * terminal_width" in order to completely clear last line terminal_width = self._terminal_width() if terminal_width: From e60da103cc40c68be8b401f2bc4c63e696210047 Mon Sep 17 00:00:00 2001 From: Rodrigo Menezes Date: Wed, 10 Aug 2016 18:48:57 -0700 Subject: [PATCH 090/143] Ensure recycling happens if bag is over capacity. (#3531) Short Description: Ensures you that item Recycling happens if you have more items than the total bag capacity. When you level up, you are awarded items which can cause the bag to be over the capacity. --- pokemongo_bot/cell_workers/recycle_items.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 15c8f7931a..3232870d03 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -28,7 +28,7 @@ def work(self): free_bag_space = total_bag_space - items_in_bag if self.min_empty_space is not None: - if free_bag_space >= self.min_empty_space: + if free_bag_space >= self.min_empty_space and items_in_bag < total_bag_space: self.emit_event( 'item_discard_skipped', formatted="Skipping Recycling of Items. {space} space left in bag.", From 00a5b2fa136a5e4b8228743a25bfb2228bda692f Mon Sep 17 00:00:00 2001 From: Amal Samally Date: Thu, 11 Aug 2016 06:44:51 +0400 Subject: [PATCH 091/143] Better inventory: attacks & movesets, IV CP perfection, pokemon level, etc.. (#3455) * Add "level to CP multiplier" data Data is from justinleewells/pogo-optimizer: https://github.com/justinleewells/pogo-optimizer/blob/edd692d/data/game/level-to-cpm.json * Many improvements & additions for the inventory logic - LevelToCPm, FastAttacks, ChargedAttacks, Movesets - More info for each pokemon: attacks data, percent to max cp, IV CP perfection * Add PyCharm/IDEA *.iml (project file) to ignored * Fixes, improvements & refactoring for inventory.py - Return inadvertently deleted pieces of code (thanks to @achretien) - Evolution logic fixes - Other minor fixes - Moveset logic moved to Moveset class * Fix data for pokemons & charged moves * Inventory tests: pokemon data, LevelToCPm, attacks * Fix travis build * Fix info for Hitmonlee & Hitmonchan --- .gitignore | 1 + data/charged_moves.json | 3 +- data/level_to_cpm.json | 81 +++++ data/pokemon.json | 32 +- pokemongo_bot/inventory.py | 676 ++++++++++++++++++++++++++++++++++--- tests/inventory_test.py | 183 ++++++++++ 6 files changed, 907 insertions(+), 69 deletions(-) create mode 100644 data/level_to_cpm.json create mode 100644 tests/inventory_test.py diff --git a/.gitignore b/.gitignore index 4721ce0253..06973c1249 100644 --- a/.gitignore +++ b/.gitignore @@ -100,6 +100,7 @@ share/ # PyCharm IDE settings .idea/ +*.iml # Personal load details src/ diff --git a/data/charged_moves.json b/data/charged_moves.json index 3d487b7201..c3f993191c 100644 --- a/data/charged_moves.json +++ b/data/charged_moves.json @@ -84,9 +84,8 @@ {"id":101,"name":"Flame Charge","type":"Fire","damage":25,"duration":3100,"energy":20,"dps":8.06}, {"id":34,"name":"Heart Stamp","type":"Psychic","damage":20,"duration":2550,"energy":25,"dps":7.84}, {"id":75,"name":"Parabolic Charge","type":"Electric","damage":15,"duration":2100,"energy":20,"dps":7.14}, -{"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":3700,"energy":20,"dps":6.75}, {"id":111,"name":"Icy Wind","type":"Ice","damage":25,"duration":3800,"energy":20,"dps":6.57}, {"id":84,"name":"Disarming Voice","type":"Fairy","damage":25,"duration":3900,"energy":20,"dps":6.41}, {"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":4000,"energy":20,"dps":6.25}, {"id":66,"name":"Shadow Sneak","type":"Ghost","damage":15,"duration":3100,"energy":20,"dps":4.83}, -{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] \ No newline at end of file +{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] diff --git a/data/level_to_cpm.json b/data/level_to_cpm.json new file mode 100644 index 0000000000..d2483d9a41 --- /dev/null +++ b/data/level_to_cpm.json @@ -0,0 +1,81 @@ +{ + "1": 0.094, + "1.5": 0.135137432, + "2": 0.16639787, + "2.5": 0.192650919, + "3": 0.21573247, + "3.5": 0.236572661, + "4": 0.25572005, + "4.5": 0.273530381, + "5": 0.29024988, + "5.5": 0.306057377, + "6": 0.3210876, + "6.5": 0.335445036, + "7": 0.34921268, + "7.5": 0.362457751, + "8": 0.37523559, + "8.5": 0.387592406, + "9": 0.39956728, + "9.5": 0.411193551, + "10": 0.42250001, + "10.5": 0.432926419, + "11": 0.44310755, + "11.5": 0.4530599578, + "12": 0.46279839, + "12.5": 0.472336083, + "13": 0.48168495, + "13.5": 0.4908558, + "14": 0.49985844, + "14.5": 0.508701765, + "15": 0.51739395, + "15.5": 0.525942511, + "16": 0.53435433, + "16.5": 0.542635767, + "17": 0.55079269, + "17.5": 0.558830576, + "18": 0.56675452, + "18.5": 0.574569153, + "19": 0.58227891, + "19.5": 0.589887917, + "20": 0.59740001, + "20.5": 0.604818814, + "21": 0.61215729, + "21.5": 0.619399365, + "22": 0.62656713, + "22.5": 0.633644533, + "23": 0.64065295, + "23.5": 0.647576426, + "24": 0.65443563, + "24.5": 0.661214806, + "25": 0.667934, + "25.5": 0.674577537, + "26": 0.68116492, + "26.5": 0.687680648, + "27": 0.69414365, + "27.5": 0.700538673, + "28": 0.70688421, + "28.5": 0.713164996, + "29": 0.71939909, + "29.5": 0.725571552, + "30": 0.7317, + "30.5": 0.734741009, + "31": 0.73776948, + "31.5": 0.740785574, + "32": 0.74378943, + "32.5": 0.746781211, + "33": 0.74976104, + "33.5": 0.752729087, + "34": 0.75568551, + "34.5": 0.758630378, + "35": 0.76156384, + "35.5": 0.764486065, + "36": 0.76739717, + "36.5": 0.770297266, + "37": 0.7731865, + "37.5": 0.776064962, + "38": 0.77893275, + "38.5": 0.781790055, + "39": 0.78463697, + "39.5": 0.787473578, + "40": 0.79030001 +} \ No newline at end of file diff --git a/data/pokemon.json b/data/pokemon.json index e64b7c31fb..8ccc906d02 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -1232,7 +1232,7 @@ "Previous evolution(s)": [ { "Number": "029", - "Name": "Nidoran F" + "Name": "Nidoran F" } ], "Next Evolution Requirements": { @@ -4326,20 +4326,14 @@ ], "Weight": "49.8 kg", "Height": "1.5 m", - "Next evolution(s)": [ - { - "Number": "107", - "Name": "Hitmonchan" - } - ], "Special Attack(s)": [ "Low Sweep", "Stomp", "Stone Edge" ], "BaseAttack": 148, - "BaseDefense": 100, - "BaseStamina": 172, + "BaseDefense": 172, + "BaseStamina": 100, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -4361,12 +4355,6 @@ ], "Weight": "50.2 kg", "Height": "1.4 m", - "Previous evolution(s)": [ - { - "Number": "106", - "Name": "Hitmonlee" - } - ], "Special Attack(s)": [ "Brick Break", "Fire Punch", @@ -4374,8 +4362,8 @@ "Thunder Punch" ], "BaseAttack": 138, - "BaseDefense": 100, - "BaseStamina": 204, + "BaseDefense": 204, + "BaseStamina": 100, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -5729,6 +5717,16 @@ "Family": 147, "Name": "Dratini candies" }, + "Next evolution(s)": [ + { + "Number": "148", + "Name": "Dragonair" + }, + { + "Number": "149", + "Name": "Dragonite" + } + ], "Special Attack(s)": [ "Aqua Tail", "Twister", diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index ea81b7c093..b27eaa388a 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1,26 +1,45 @@ import json import os + from pokemongo_bot.base_dir import _base_dir ''' Helper class for updating/retrieving Inventory data ''' -class _BaseInventoryComponent(object): - TYPE = None # base key name for items of this type - ID_FIELD = None # identifier field for items of this type + +# +# Abstraction + +class _StaticInventoryComponent(object): STATIC_DATA_FILE = None # optionally load static data from file, # dropping the data in a static variable named STATIC_DATA + STATIC_DATA = None def __init__(self): - self._data = {} if self.STATIC_DATA_FILE is not None: self.init_static_data() @classmethod def init_static_data(cls): if not hasattr(cls, 'STATIC_DATA') or cls.STATIC_DATA is None: - cls.STATIC_DATA = json.load(open(cls.STATIC_DATA_FILE)) + cls.STATIC_DATA = cls.process_static_data( + json.load(open(cls.STATIC_DATA_FILE))) + + @classmethod + def process_static_data(cls, data): + # optional hook for processing the static data + # default is to use the data directly + return data + + +class _BaseInventoryComponent(_StaticInventoryComponent): + TYPE = None # base key name for items of this type + ID_FIELD = None # identifier field for items of this type + + def __init__(self): + self._data = {} + super(_BaseInventoryComponent, self).__init__() def parse(self, item): # optional hook for parsing the dict for this item @@ -42,34 +61,22 @@ def retrieve_data(self, inventory): def refresh(self, inventory): self._data = self.retrieve_data(inventory) - def get(self, id): - return self._data.get(id) + def get(self, object_id): + return self._data.get(object_id) def all(self): return list(self._data.values()) -class Candy(object): - def __init__(self, family_id, quantity): - self.type = Pokemons.name_for(family_id) - self.quantity = quantity - - def consume(self, amount): - if self.quantity < amount: - raise Exception('Tried to consume more {} candy than you have'.format(self.type)) - self.quantity -= amount - - def add(self, amount): - if amount < 0: - raise Exception('Must add positive amount of candy') - self.quantity += amount +# +# Inventory Components class Candies(_BaseInventoryComponent): TYPE = 'candy' ID_FIELD = 'family_id' @classmethod - def family_id_for(self, pokemon_id): + def family_id_for(cls, pokemon_id): return Pokemons.first_evolution_id_for(pokemon_id) def get(self, pokemon_id): @@ -108,17 +115,120 @@ class Pokemons(_BaseInventoryComponent): ID_FIELD = 'id' STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'pokemon.json') - def parse(self, item): - if 'is_egg' in item: - return Egg(item) - return Pokemon(item) + @classmethod + def process_static_data(cls, data): + pokemon_id = 1 + for poke_info in data: + # prepare types + types = [poke_info['Type I'][0]] # required + for t in poke_info.get('Type II', []): + types.append(t) + poke_info['types'] = types + + # prepare attacks (moves) + cls._process_attacks(poke_info) + cls._process_attacks(poke_info, charged=True) + + # prepare movesets + poke_info['movesets'] = cls._process_movesets(poke_info, pokemon_id) + + # calculate maximum CP for the pokemon (best IVs, lvl 40) + base_attack = poke_info['BaseAttack'] + base_defense = poke_info['BaseDefense'] + base_stamina = poke_info['BaseStamina'] + max_cp = _calc_cp(base_attack, base_defense, base_stamina) + poke_info['max_cp'] = max_cp + + pokemon_id += 1 + return data + + @classmethod + def _process_movesets(cls, poke_info, pokemon_id): + # type: (dict, int) -> List[Moveset] + """ + The optimal moveset is the combination of two moves, one quick move + and one charge move, that deals the most damage over time. + + Because each quick move gains a certain amount of energy (different + for different moves) and each charge move requires a different amount + of energy to use, sometimes, a quick move with lower DPS will be + better since it charges the charge move faster. On the same note, + sometimes a charge move that has lower DPS will be more optimal since + it may require less energy or it may last for a longer period of time. + + Attacker have STAB (Same-type attack bonus - x1.25) pokemon have the + same type as attack. So we add it to the "Combo DPS" of the moveset. + + The defender attacks in intervals of 1 second for the first 2 attacks, + and then in intervals of 2 seconds for the remainder of the attacks. + This explains why we see two consecutive quick attacks at the beginning + of the match. As a result, we add +2 seconds to the DPS calculation + for defender DPS output. + + So to determine an optimal defensive moveset, we follow the same method + as we did for optimal offensive movesets, but instead calculate the + highest "Combo DPS" with an added 2 seconds to the quick move cool down. + + Note: critical hits have not yet been implemented in the game + + See http://pokemongo.gamepress.gg/optimal-moveset-explanation + See http://pokemongo.gamepress.gg/defensive-tactics + """ + + # Prepare movesets + movesets = [] + types = poke_info['types'] + for fm in poke_info['Fast Attack(s)']: + for chm in poke_info['Special Attack(s)']: + movesets.append(Moveset(fm, chm, types, pokemon_id)) + assert len(movesets) > 0 + + # Calculate attack perfection for each moveset + movesets = sorted(movesets, key=lambda m: m.dps_attack) + worst_dps = movesets[0].dps_attack + best_dps = movesets[-1].dps_attack + if best_dps > worst_dps: + for moveset in movesets: + current_dps = moveset.dps_attack + moveset.attack_perfection = \ + (current_dps - worst_dps) / (best_dps - worst_dps) + + # Calculate defense perfection for each moveset + movesets = sorted(movesets, key=lambda m: m.dps_defense) + worst_dps = movesets[0].dps_defense + best_dps = movesets[-1].dps_defense + if best_dps > worst_dps: + for moveset in movesets: + current_dps = moveset.dps_defense + moveset.defense_perfection = \ + (current_dps - worst_dps) / (best_dps - worst_dps) + + return sorted(movesets, key=lambda m: m.dps, reverse=True) + + @classmethod + def _process_attacks(cls, poke_info, charged=False): + # type: (dict, bool) -> List[Attack] + key = 'Fast Attack(s)' if not charged else 'Special Attack(s)' + moves_dict = (ChargedAttacks if charged else FastAttacks).BY_NAME + moves = [] + for name in poke_info[key]: + if name not in moves_dict: + raise KeyError('Unknown {} attack: "{}"'.format( + 'charged' if charged else 'fast', name)) + moves.append(moves_dict[name]) + moves = sorted(moves, key=lambda m: m.dps, reverse=True) + poke_info[key] = moves + assert len(moves) > 0 + return moves @classmethod def data_for(cls, pokemon_id): + # type: (int) -> dict return cls.STATIC_DATA[pokemon_id - 1] @classmethod def name_for(cls, pokemon_id): + # type: (int) -> string return cls.data_for(pokemon_id)['Name'] @classmethod @@ -129,24 +239,194 @@ def first_evolution_id_for(cls, pokemon_id): return pokemon_id @classmethod - def next_evolution_id_for(cls, pokemon_id): + def prev_evolution_id_for(cls, pokemon_id): + data = cls.data_for(pokemon_id) + if 'Previous evolution(s)' in data: + return int(data['Previous evolution(s)'][-1]['Number']) + return None + + @classmethod + def next_evolution_ids_for(cls, pokemon_id): try: - return int(cls.data_for(pokemon_id)['Next evolution(s)'][0]['Number']) + next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] except KeyError: - return None + return [] + # get only next level evolutions, not all possible + ids = [] + for p in next_evolutions: + p_id = int(p['Number']) + if cls.prev_evolution_id_for(p_id) == pokemon_id: + ids.append(p_id) + return ids @classmethod - def evolution_cost_for(cls, pokemon_id): + def last_evolution_ids_for(cls, pokemon_id): try: - return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] except KeyError: - return + return [pokemon_id] + # get only final evolutions, not all possible + ids = [] + for p in next_evolutions: + p_id = int(p['Number']) + if len(cls.data_for(p_id).get('Next evolution(s)', [])) == 0: + ids.append(p_id) + assert len(ids) > 0 + return ids + + @classmethod + def has_next_evolution(cls, pokemon_id): + poke_info = cls.data_for(pokemon_id) + return 'Next Evolution Requirements' in poke_info \ + or 'Next evolution(s)' in poke_info + + @classmethod + def evolution_cost_for(cls, pokemon_id): + if not cls.has_next_evolution(pokemon_id): + return None + return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + + def parse(self, item): + if 'is_egg' in item: + return Egg(item) + return Pokemon(item) def all(self): # by default don't include eggs in all pokemon (usually just # makes caller's lives more difficult) return [p for p in super(Pokemons, self).all() if not isinstance(p, Egg)] + +# +# Static Components + +class LevelToCPm(_StaticInventoryComponent): + """ + Data for the CP multipliers at different levels + See http://pokemongo.gamepress.gg/cp-multiplier + See https://github.com/justinleewells/pogo-optimizer/blob/edd692d/data/game/level-to-cpm.json + """ + + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'level_to_cpm.json') + MAX_LEVEL = 40 + MAX_CPM = .0 + # half of the lowest difference between CPMs + HALF_DIFF_BETWEEN_HALF_LVL = 14e-3 + + @classmethod + def init_static_data(cls): + super(LevelToCPm, cls).init_static_data() + cls.MAX_CPM = cls.cp_multiplier_for(cls.MAX_LEVEL) + + @classmethod + def cp_multiplier_for(cls, level): + # type: (Union[float, int, string]) -> float + level = float(level) + level = str(int(level) if level.is_integer() else level) + return cls.STATIC_DATA[level] + + @classmethod + def level_from_cpm(cls, cp_multiplier): + # type: (float) -> float + for lvl, cpm in cls.STATIC_DATA.iteritems(): + diff = abs(cpm - cp_multiplier) + if diff <= cls.HALF_DIFF_BETWEEN_HALF_LVL: + return float(lvl) + raise ValueError("Unknown cp_multiplier: {}".format(cp_multiplier)) + + +class _Attacks(_StaticInventoryComponent): + BY_NAME = {} # type: Dict[string, Attack] + BY_TYPE = {} # type: Dict[List[Attack]] + BY_DPS = [] # type: List[Attack] + + @classmethod + def process_static_data(cls, moves): + ret = {} + by_type = {} + by_name = {} + fast = cls is FastAttacks + for attack in moves: + attack = Attack(attack) if fast else ChargedAttack(attack) + ret[attack.id] = attack + by_name[attack.name] = attack + + if attack.type not in by_type: + by_type[attack.type] = [] + by_type[attack.type].append(attack) + + for t in by_type.iterkeys(): + attacks = sorted(by_type[t], key=lambda m: m.dps, reverse=True) + min_dps = attacks[-1].dps + max_dps = attacks[0].dps - min_dps + if max_dps > .0: + for attack in attacks: # type: Attack + attack.rate_in_type = (attack.dps - min_dps) / max_dps + by_type[t] = attacks + + cls.BY_NAME = by_name + cls.BY_TYPE = by_type + cls.BY_DPS = sorted(ret.values(), key=lambda m: m.dps, reverse=True) + + return ret + + @classmethod + def data_for(cls, attack_id): + # type: (int) -> Attack + if attack_id not in cls.STATIC_DATA: + raise ValueError("Attack {} not found in {}".format( + attack_id, cls.__name__)) + return cls.STATIC_DATA[attack_id] + + @classmethod + def by_name(cls, name): + # type: (string) -> Attack + return cls.BY_NAME[name] + + @classmethod + def list_for_type(cls, type_name): + # type: (string) -> List[Attack] + """ + :return: Attacks sorted by DPS in descending order + """ + return cls.BY_TYPE[type_name] + + @classmethod + def all(cls): + return cls.STATIC_DATA.values() + + @classmethod + def all_by_dps(cls): + return cls.BY_DPS + + +class FastAttacks(_Attacks): + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'fast_moves.json') + + +class ChargedAttacks(_Attacks): + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'charged_moves.json') + + +# +# Instances + +class Candy(object): + def __init__(self, family_id, quantity): + self.type = Pokemons.name_for(family_id) + self.quantity = quantity + + def consume(self, amount): + if self.quantity < amount: + raise Exception('Tried to consume more {} candy than you have'.format(self.type)) + self.quantity -= amount + + def add(self, amount): + if amount < 0: + raise Exception('Must add positive amount of candy') + self.quantity += amount + + class Egg(object): def __init__(self, data): self._data = data @@ -158,52 +438,297 @@ def has_next_evolution(self): class Pokemon(object): def __init__(self, data): self._data = data + # Unique ID for this particular Pokemon self.id = data['id'] + # Id of the such pokemons in pokedex self.pokemon_id = data['pokemon_id'] + + # Combat points value self.cp = data['cp'] + # Base CP multiplier, fixed at the catch time + self.cp_bm = data['cp_multiplier'] + # Changeable part of the CP multiplier, increasing at power up + self.cp_am = data.get('additional_cp_multiplier', .0) + # Resulting CP multiplier + self.cp_m = self.cp_bm + self.cp_am + + # Current pokemon level (half of level is a normal value) + self.level = LevelToCPm.level_from_cpm(self.cp_m) + + # Maximum health points + self.hp_max = data['stamina_max'] + # Current health points + self.hp = data.get('stamina', self.hp_max) + assert 0 <= self.hp <= self.hp_max + + # Individial Values of the current pokemon (different for each pokemon) + self.iv_attack = data.get('individual_attack', 0) + self.iv_defense = data.get('individual_defense', 0) + self.iv_stamina = data.get('individual_stamina', 0) + self._static_data = Pokemons.data_for(self.pokemon_id) self.name = Pokemons.name_for(self.pokemon_id) - self.iv = self._compute_iv() + self.nickname = data.get('nickname', self.name) + self.in_fort = 'deployed_fort_id' in data self.is_favorite = data.get('favorite', 0) is 1 + # Basic Values of the current pokemon (identical for all such pokemons) + self.base_attack = self._static_data['BaseAttack'] + self.base_defense = self._static_data['BaseDefense'] + self.base_stamina = self._static_data['BaseStamina'] + + # Maximum possible CP for the current pokemon + self.max_cp = self._static_data['max_cp'] + + self.fast_attack = FastAttacks.data_for(data['move_1']) + self.charged_attack = ChargedAttacks.data_for(data['move_2']) + + # Internal values (IV) perfection percent + self.iv = self._compute_iv_perfection() + + # IV CP perfection - kind of IV perfection percent but calculated + # using weight of each IV in its contribution to CP of the best + # evolution of current pokemon + # So it tends to be more accurate than simple IV perfection + self.ivcp = self._compute_cp_perfection() + + # Exact value of current CP (not rounded) + self.cp_exact = _calc_cp( + self.base_attack, self.base_defense, self.base_stamina, + self.iv_attack, self.iv_defense, self.iv_stamina, self.cp_m) + + # Percent of maximum possible CP + self.cp_percent = self.cp_exact / self.max_cp + + # Get moveset instance with calculated DPS and perfection percents + self.moveset = self._get_moveset() + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + def can_evolve_now(self): - return self.has_next_evolution() and self.candy_quantity >= self.evolution_cost + return self.has_next_evolution() and \ + self.candy_quantity >= self.evolution_cost def has_next_evolution(self): - return 'Next Evolution Requirements' in self._static_data + return Pokemons.has_next_evolution(self.pokemon_id) def has_seen_next_evolution(self): - return pokedex().captured(self.next_evolution_id) + for pokemon_id in self.next_evolution_ids: + if pokedex().captured(pokemon_id): + return True + return False @property - def next_evolution_id(self): - return Pokemons.next_evolution_id_for(self.pokemon_id) + def family_id(self): + return self.first_evolution_id @property def first_evolution_id(self): return Pokemons.first_evolution_id_for(self.pokemon_id) + @property + def prev_evolution_id(self): + return Pokemons.prev_evolution_id_for(self.pokemon_id) + + @property + def next_evolution_ids(self): + return Pokemons.next_evolution_ids_for(self.pokemon_id) + + @property + def last_evolution_ids(self): + return Pokemons.last_evolution_ids_for(self.pokemon_id) + @property def candy_quantity(self): return candies().get(self.pokemon_id).quantity @property def evolution_cost(self): - return self._static_data['Next Evolution Requirements']['Amount'] + return Pokemons.evolution_cost_for(self.pokemon_id) + + def _compute_iv_perfection(self): + total_iv = self.iv_attack + self.iv_defense + self.iv_stamina + iv_perfection = round((total_iv / 45.0), 2) + return iv_perfection + + def _compute_cp_perfection(self): + """ + CP perfect percent is more accurate than IV perfect + + We know attack plays an important role in CP, and different + pokemons have different base value, that's means 15/14/15 is + better than 14/15/15 for lot of pokemons, and if one pokemon's + base def is more than base sta, 15/15/14 is better than 15/14/15. + + See https://github.com/jabbink/PokemonGoBot/issues/469 + + So calculate CP perfection at final level for the best of the final + evolutions of the pokemon. + """ + variants = [] + iv_attack = self.iv_attack + iv_defense = self.iv_defense + iv_stamina = self.iv_stamina + cp_m = LevelToCPm.MAX_CPM + last_evolution_ids = self.last_evolution_ids + for pokemon_id in last_evolution_ids: + poke_info = Pokemons.data_for(pokemon_id) + base_attack = poke_info['BaseAttack'] + base_defense = poke_info['BaseDefense'] + base_stamina = poke_info['BaseStamina'] + + # calculate CP variants at maximum level + worst_cp = _calc_cp(base_attack, base_defense, base_stamina, + 0, 0, 0, cp_m) + perfect_cp = _calc_cp(base_attack, base_defense, base_stamina, + cp_multiplier=cp_m) + current_cp = _calc_cp(base_attack, base_defense, base_stamina, + iv_attack, iv_defense, iv_stamina, cp_m) + cp_perfection = (current_cp - worst_cp) / (perfect_cp - worst_cp) + variants.append(cp_perfection) + + # get best value (probably for the best evolution) + cp_perfection = max(variants) + return cp_perfection + + def _get_moveset(self): + move1 = self.fast_attack + move2 = self.charged_attack + movesets = self._static_data['movesets'] + current_moveset = None + for moveset in movesets: # type: Moveset + if moveset.fast_attack == move1 and moveset.charged_attack == move2: + current_moveset = moveset + break + + if current_moveset is None: + raise Exception("Unexpected moveset [{}, {}] for #{} {}".format( + move1, move2, self.pokemon_id, self.name)) + + return current_moveset + + +class Attack(object): + def __init__(self, data): + # self._data = data # Not needed - all saved in fields + self.id = data['id'] + self.name = data['name'] + self.type = data['type'] + self.damage = data['damage'] + self.duration = data['duration'] / 1000.0 # duration in seconds + + # Energy addition for fast attack + # Energy cost for charged attack + self.energy = data['energy'] + + # Damage Per Second + # recalc for better precision + self.dps = self.damage / self.duration + + # Perfection of the attack in it's type (from 0 to 1) + self.rate_in_type = .0 + + @property + def damage_with_stab(self): + # damage with STAB (Same-type attack bonus) + return self.damage * STAB_FACTOR + + @property + def dps_with_stab(self): + # DPS with STAB (Same-type attack bonus) + return self.dps * STAB_FACTOR + + @property + def energy_per_second(self): + return self.energy / self.duration + + @property + def dodge_window(self): + # TODO: Attack Dodge Window + return NotImplemented + + @property + def is_charged(self): + return False + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + + +class ChargedAttack(Attack): + def __init__(self, data): + super(ChargedAttack, self).__init__(data) + + @property + def is_charged(self): + return True + + +class Moveset(object): + def __init__(self, fm, chm, pokemon_types=(), pokemon_id=-1): + # type: (Attack, ChargedAttack, List[string], int) -> None + self.pokemon_id = pokemon_id + self.fast_attack = fm + self.charged_attack = chm + + # See Pokemons._process_movesets() + # See http://pokemongo.gamepress.gg/optimal-moveset-explanation + # See http://pokemongo.gamepress.gg/defensive-tactics + + fm_number = 100 # for simplicity we use 100 + + fm_energy = fm.energy * fm_number + fm_damage = fm.damage * fm_number + fm_secs = fm.duration * fm_number + + # Defender attacks in intervals of 1 second for the + # first 2 attacks, and then in intervals of 2 seconds + # So add 1.95 seconds to the quick move cool down for defense + # 1.95 is something like an average here + # TODO: Do something better? + fm_defense_secs = (fm.duration + 1.95) * fm_number + + chm_number = fm_energy / chm.energy + chm_damage = chm.damage * chm_number + chm_secs = chm.duration * chm_number + + damage_sum = fm_damage + chm_damage + # raw Damage-Per-Second for the moveset + self.dps = damage_sum / (fm_secs + chm_secs) + # average DPS for defense + self.dps_defense = damage_sum / (fm_defense_secs + chm_secs) + + # apply STAB (Same-type attack bonus) + if fm.type in pokemon_types: + fm_damage *= STAB_FACTOR + if chm.type in pokemon_types: + chm_damage *= STAB_FACTOR + + # DPS for attack (counting STAB) + self.dps_attack = (fm_damage + chm_damage) / (fm_secs + chm_secs) - def _compute_iv(self): - total_IV = 0.0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] + # Moveset perfection percent attack and for defense + # Calculated for current pokemon, not between all pokemons + # So 100% perfect moveset can be weak if pokemon is weak (e.g. Caterpie) + self.attack_perfection = .0 + self.defense_perfection = .0 - for individual_stat in iv_stats: - try: - total_IV += self._data[individual_stat] - except Exception: - self._data[individual_stat] = 0 - continue - pokemon_potential = round((total_IV / 45.0), 2) - return pokemon_potential + # TODO: True DPS for real combat (floor(Attack/200 * MovePower * STAB) + 1) + # See http://pokemongo.gamepress.gg/pokemon-attack-explanation + + def __str__(self): + return '[{}, {}]'.format(self.fast_attack, self.charged_attack) + + def __repr__(self): + return '[{}, {}]'.format(self.fast_attack, self.charged_attack) class Inventory(object): @@ -227,8 +752,47 @@ def refresh(self): with open(user_web_inventory, 'w') as outfile: json.dump(inventory, outfile) +# +# Usage helpers + +# STAB (Same-type attack bonus) +STAB_FACTOR = 1.25 _inventory = None +LevelToCPm() # init LevelToCPm +FastAttacks() # init FastAttacks +ChargedAttacks() # init ChargedAttacks + + +def _calc_cp(base_attack, base_defense, base_stamina, + iv_attack=15, iv_defense=15, iv_stamina=15, + cp_multiplier=LevelToCPm.MAX_CPM): + """ + CP calculation + + CP = (Attack * Defense^0.5 * Stamina^0.5 * CP_Multiplier^2) / 10 + CP = (BaseAtk+AtkIV) * (BaseDef+DefIV)^0.5 * (BaseStam+StamIV)^0.5 * Lvl(CPScalar)^2 / 10 + + See https://www.reddit.com/r/TheSilphRoad/comments/4t7r4d/exact_pokemon_cp_formula/ + See https://www.reddit.com/r/pokemongodev/comments/4t7xb4/exact_cp_formula_from_stats_and_cpm_and_an_update/ + See http://pokemongo.gamepress.gg/pokemon-stats-advanced + See http://pokemongo.gamepress.gg/cp-multiplier + See http://gaming.stackexchange.com/questions/280491/formula-to-calculate-pokemon-go-cp-and-hp + + :param base_attack: Pokemon BaseAttack + :param base_defense: Pokemon BaseDefense + :param base_stamina: Pokemon BaseStamina + :param iv_attack: Pokemon IndividualAttack (0..15) + :param iv_defense: Pokemon IndividualDefense (0..15) + :param iv_stamina: Pokemon IndividualStamina (0..15) + :param cp_multiplier: CP Multiplier (0.79030001 is max - value for level 40) + :return: CP as float + """ + return (base_attack + iv_attack) \ + * ((base_defense + iv_defense)**0.5) \ + * ((base_stamina + iv_stamina)**0.5) \ + * (cp_multiplier ** 2) / 10 + def init_inventory(bot): global _inventory @@ -257,3 +821,15 @@ def pokemons(refresh=False): def items(): return _inventory.items + + +def levels_to_cpm(): + return LevelToCPm + + +def fast_attacks(): + return FastAttacks + + +def charged_attacks(): + return ChargedAttacks diff --git a/tests/inventory_test.py b/tests/inventory_test.py new file mode 100644 index 0000000000..8362ce2b91 --- /dev/null +++ b/tests/inventory_test.py @@ -0,0 +1,183 @@ +import unittest + +from pokemongo_bot.inventory import * + + +class InventoryTest(unittest.TestCase): + def test_pokemons(self): + # Init data + self.assertEqual(len(Pokemons().all()), 0) # No inventory loaded here + + obj = Pokemons + self.assertEqual(len(obj.STATIC_DATA), 151) + + for poke_info in obj.STATIC_DATA: + name = poke_info['Name'] + pokemon_id = int(poke_info['Number']) + self.assertTrue(1 <= pokemon_id <= 151) + + self.assertGreaterEqual(len(poke_info['movesets']), 1) + self.assertTrue(262 <= poke_info['max_cp'] <= 4145) + self.assertTrue(1 <= len(poke_info['types']) <= 2) + self.assertTrue(40 <= poke_info['BaseAttack'] <= 284) + self.assertTrue(20 <= poke_info['BaseDefense'] <= 500) + self.assertTrue(54 <= poke_info['BaseStamina'] <= 242) + self.assertTrue(.0 <= poke_info['CaptureRate'] <= .56) + self.assertTrue(.0 <= poke_info['FleeRate'] <= .99) + self.assertTrue(1 <= len(poke_info['Weaknesses']) <= 7) + self.assertTrue(3 <= len(name) <= 10) + + self.assertGreaterEqual(len(poke_info['Classification']), 11) + self.assertGreaterEqual(len(poke_info['Fast Attack(s)']), 1) + self.assertGreaterEqual(len(poke_info['Special Attack(s)']), 1) + + self.assertIs(obj.data_for(pokemon_id), poke_info) + self.assertIs(obj.name_for(pokemon_id), name) + + first_evolution_id = obj.first_evolution_id_for(pokemon_id) + self.assertGreaterEqual(first_evolution_id, 1) + next_evolution_ids = obj.next_evolution_ids_for(pokemon_id) + last_evolution_ids = obj.last_evolution_ids_for(pokemon_id) + candies_cost = obj.evolution_cost_for(pokemon_id) + obj.prev_evolution_id_for(pokemon_id) # just call test + self.assertGreaterEqual(len(last_evolution_ids), 1) + + if not obj.has_next_evolution(pokemon_id): + assert 'Next evolution(s)' not in poke_info + assert 'Next Evolution Requirements' not in poke_info + else: + self.assertGreaterEqual(len(next_evolution_ids), 1) + self.assertLessEqual(len(next_evolution_ids), len(last_evolution_ids)) + + reqs = poke_info['Next Evolution Requirements'] + self.assertEqual(reqs["Family"], first_evolution_id) + candies_name = obj.name_for(first_evolution_id) + ' candies' + self.assertEqual(reqs["Name"], candies_name) + self.assertIsNotNone(candies_cost) + self.assertTrue(12 <= candies_cost <= 400) + self.assertEqual(reqs["Amount"], candies_cost) + + evolutions = poke_info["Next evolution(s)"] + self.assertGreaterEqual(len(evolutions), len(next_evolution_ids)) + + for p in evolutions: + p_id = int(p["Number"]) + self.assertNotEqual(p_id, pokemon_id) + self.assertEqual(p["Name"], obj.name_for(p_id)) + + for p_id in next_evolution_ids: + self.assertEqual(obj.prev_evolution_id_for(p_id), pokemon_id) + prev_evs = obj.data_for(p_id)["Previous evolution(s)"] + self.assertGreaterEqual(len(prev_evs), 1) + self.assertEqual(int(prev_evs[-1]["Number"]), pokemon_id) + self.assertEqual(prev_evs[-1]["Name"], name) + + # Only Eevee has 3 next evolutions + self.assertEqual(len(next_evolution_ids), + 1 if pokemon_id != 133 else 3) + + if "Previous evolution(s)" in poke_info: + for p in poke_info["Previous evolution(s)"]: + p_id = int(p["Number"]) + self.assertNotEqual(p_id, pokemon_id) + self.assertEqual(p["Name"], obj.name_for(p_id)) + + # + # Specific pokemons testing + + poke = Pokemon({ + "num_upgrades": 2, "move_1": 210, "move_2": 69, "pokeball": 2, + "favorite": 1, "pokemon_id": 42, "battles_attacked": 4, + "stamina": 76, "stamina_max": 76, "individual_attack": 9, + "individual_defense": 4, "individual_stamina": 8, + "cp_multiplier": 0.4627983868122101, + "additional_cp_multiplier": 0.018886566162109375, + "cp": 653, "nickname": "Golb", "id": 13632861873471324}) + self.assertEqual(poke.level, 12.5) + self.assertEqual(poke.iv, 0.47) + self.assertAlmostEqual(poke.ivcp, 0.482845351) + self.assertAlmostEqual(poke.max_cp, 1921.34561459) + self.assertAlmostEqual(poke.cp_percent, 0.34000973) + self.assertTrue(poke.is_favorite) + self.assertEqual(poke.name, 'Golbat') + self.assertEqual(poke.nickname, "Golb") + self.assertAlmostEqual(poke.moveset.dps, 10.7540173053) + self.assertAlmostEqual(poke.moveset.dps_attack, 12.14462299) + self.assertAlmostEqual(poke.moveset.dps_defense, 4.876681614) + self.assertAlmostEqual(poke.moveset.attack_perfection, 0.4720730048) + self.assertAlmostEqual(poke.moveset.defense_perfection, 0.8158081497) + + poke = Pokemon({ + "move_1": 221, "move_2": 129, "pokemon_id": 19, "cp": 110, + "individual_attack": 6, "stamina_max": 22, "individual_defense": 14, + "cp_multiplier": 0.37523558735847473, "id": 7841053399}) + self.assertEqual(poke.level, 7.5) + self.assertEqual(poke.iv, 0.44) + self.assertAlmostEqual(poke.ivcp, 0.452398293) + self.assertAlmostEqual(poke.max_cp, 581.64643575) + self.assertAlmostEqual(poke.cp_percent, 0.189251848608) + self.assertFalse(poke.is_favorite) + self.assertEqual(poke.name, 'Rattata') + self.assertEqual(poke.nickname, 'Rattata') + self.assertAlmostEqual(poke.moveset.dps, 12.5567813108) + self.assertAlmostEqual(poke.moveset.dps_attack, 15.6959766385) + self.assertAlmostEqual(poke.moveset.dps_defense, 5.54282440561) + self.assertAlmostEqual(poke.moveset.attack_perfection, 0.835172881385) + self.assertAlmostEqual(poke.moveset.defense_perfection, 0.603137650999) + + def test_levels_to_cpm(self): + l2c = LevelToCPm + self.assertIs(levels_to_cpm(), l2c) + max_cpm = l2c.cp_multiplier_for(l2c.MAX_LEVEL) + self.assertEqual(l2c.MAX_LEVEL, 40) + self.assertEqual(l2c.MAX_CPM, max_cpm) + self.assertEqual(len(l2c.STATIC_DATA), 79) + + self.assertEqual(l2c.cp_multiplier_for("1"), 0.094) + self.assertEqual(l2c.cp_multiplier_for(1), 0.094) + self.assertEqual(l2c.cp_multiplier_for(1.0), 0.094) + self.assertEqual(l2c.cp_multiplier_for("17.5"), 0.558830576) + self.assertEqual(l2c.cp_multiplier_for(17.5), 0.558830576) + self.assertEqual(l2c.cp_multiplier_for('40.0'), 0.79030001) + self.assertEqual(l2c.cp_multiplier_for(40.0), 0.79030001) + self.assertEqual(l2c.cp_multiplier_for(40), 0.79030001) + + self.assertEqual(l2c.level_from_cpm(0.79030001), 40.0) + self.assertEqual(l2c.level_from_cpm(0.7903), 40.0) + + def test_attacks(self): + self._test_attacks(fast_attacks, FastAttacks) + self._test_attacks(charged_attacks, ChargedAttacks) + + def _test_attacks(self, callback, clazz): + charged = clazz is ChargedAttacks + self.assertIs(callback(), clazz) + + # check consistency + attacks = clazz.all_by_dps() + number = len(attacks) + self.assertTrue(number > 0) + self.assertGreaterEqual(len(clazz.BY_TYPE), 17) + self.assertEqual(number, len(clazz.all())) + self.assertEqual(number, len(clazz.STATIC_DATA)) + self.assertEqual(number, len(clazz.BY_NAME)) + self.assertEqual(number, sum([len(l) for l in clazz.BY_TYPE.values()])) + + # check data + prev_dps = float("inf") + for attack in attacks: # type: Attack + self.assertGreater(attack.id, 0) + self.assertGreater(len(attack.name), 0) + self.assertGreater(len(attack.type), 0) + self.assertGreaterEqual(attack.damage, 0) + self.assertGreater(attack.duration, .0) + self.assertGreater(attack.energy, 0) + self.assertGreaterEqual(attack.dps, 0) + self.assertTrue(.0 <= attack.rate_in_type <= 1.0) + self.assertLessEqual(attack.dps, prev_dps) + self.assertEqual(attack.is_charged, charged) + self.assertIs(attack, clazz.data_for(attack.id)) + self.assertIs(attack, clazz.by_name(attack.name)) + self.assertTrue(attack in clazz.BY_TYPE[attack.type]) + self.assertIsInstance(attack, ChargedAttack if charged else Attack) + prev_dps = attack.dps From e9b229ec0fd14a4814ea7431b1256850e907cfbf Mon Sep 17 00:00:00 2001 From: Nikhil Pandey Date: Thu, 11 Aug 2016 09:34:11 +0545 Subject: [PATCH 092/143] Revert "Better inventory: attacks & movesets, IV CP perfection, pokemon level, etc.." (#3549) --- .gitignore | 1 - data/charged_moves.json | 3 +- data/level_to_cpm.json | 81 ----- data/pokemon.json | 32 +- pokemongo_bot/inventory.py | 676 +++---------------------------------- tests/inventory_test.py | 183 ---------- 6 files changed, 69 insertions(+), 907 deletions(-) delete mode 100644 data/level_to_cpm.json delete mode 100644 tests/inventory_test.py diff --git a/.gitignore b/.gitignore index 06973c1249..4721ce0253 100644 --- a/.gitignore +++ b/.gitignore @@ -100,7 +100,6 @@ share/ # PyCharm IDE settings .idea/ -*.iml # Personal load details src/ diff --git a/data/charged_moves.json b/data/charged_moves.json index c3f993191c..3d487b7201 100644 --- a/data/charged_moves.json +++ b/data/charged_moves.json @@ -84,8 +84,9 @@ {"id":101,"name":"Flame Charge","type":"Fire","damage":25,"duration":3100,"energy":20,"dps":8.06}, {"id":34,"name":"Heart Stamp","type":"Psychic","damage":20,"duration":2550,"energy":25,"dps":7.84}, {"id":75,"name":"Parabolic Charge","type":"Electric","damage":15,"duration":2100,"energy":20,"dps":7.14}, +{"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":3700,"energy":20,"dps":6.75}, {"id":111,"name":"Icy Wind","type":"Ice","damage":25,"duration":3800,"energy":20,"dps":6.57}, {"id":84,"name":"Disarming Voice","type":"Fairy","damage":25,"duration":3900,"energy":20,"dps":6.41}, {"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":4000,"energy":20,"dps":6.25}, {"id":66,"name":"Shadow Sneak","type":"Ghost","damage":15,"duration":3100,"energy":20,"dps":4.83}, -{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] +{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] \ No newline at end of file diff --git a/data/level_to_cpm.json b/data/level_to_cpm.json deleted file mode 100644 index d2483d9a41..0000000000 --- a/data/level_to_cpm.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "1": 0.094, - "1.5": 0.135137432, - "2": 0.16639787, - "2.5": 0.192650919, - "3": 0.21573247, - "3.5": 0.236572661, - "4": 0.25572005, - "4.5": 0.273530381, - "5": 0.29024988, - "5.5": 0.306057377, - "6": 0.3210876, - "6.5": 0.335445036, - "7": 0.34921268, - "7.5": 0.362457751, - "8": 0.37523559, - "8.5": 0.387592406, - "9": 0.39956728, - "9.5": 0.411193551, - "10": 0.42250001, - "10.5": 0.432926419, - "11": 0.44310755, - "11.5": 0.4530599578, - "12": 0.46279839, - "12.5": 0.472336083, - "13": 0.48168495, - "13.5": 0.4908558, - "14": 0.49985844, - "14.5": 0.508701765, - "15": 0.51739395, - "15.5": 0.525942511, - "16": 0.53435433, - "16.5": 0.542635767, - "17": 0.55079269, - "17.5": 0.558830576, - "18": 0.56675452, - "18.5": 0.574569153, - "19": 0.58227891, - "19.5": 0.589887917, - "20": 0.59740001, - "20.5": 0.604818814, - "21": 0.61215729, - "21.5": 0.619399365, - "22": 0.62656713, - "22.5": 0.633644533, - "23": 0.64065295, - "23.5": 0.647576426, - "24": 0.65443563, - "24.5": 0.661214806, - "25": 0.667934, - "25.5": 0.674577537, - "26": 0.68116492, - "26.5": 0.687680648, - "27": 0.69414365, - "27.5": 0.700538673, - "28": 0.70688421, - "28.5": 0.713164996, - "29": 0.71939909, - "29.5": 0.725571552, - "30": 0.7317, - "30.5": 0.734741009, - "31": 0.73776948, - "31.5": 0.740785574, - "32": 0.74378943, - "32.5": 0.746781211, - "33": 0.74976104, - "33.5": 0.752729087, - "34": 0.75568551, - "34.5": 0.758630378, - "35": 0.76156384, - "35.5": 0.764486065, - "36": 0.76739717, - "36.5": 0.770297266, - "37": 0.7731865, - "37.5": 0.776064962, - "38": 0.77893275, - "38.5": 0.781790055, - "39": 0.78463697, - "39.5": 0.787473578, - "40": 0.79030001 -} \ No newline at end of file diff --git a/data/pokemon.json b/data/pokemon.json index 8ccc906d02..e64b7c31fb 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -1232,7 +1232,7 @@ "Previous evolution(s)": [ { "Number": "029", - "Name": "Nidoran F" + "Name": "Nidoran F" } ], "Next Evolution Requirements": { @@ -4326,14 +4326,20 @@ ], "Weight": "49.8 kg", "Height": "1.5 m", + "Next evolution(s)": [ + { + "Number": "107", + "Name": "Hitmonchan" + } + ], "Special Attack(s)": [ "Low Sweep", "Stomp", "Stone Edge" ], "BaseAttack": 148, - "BaseDefense": 172, - "BaseStamina": 100, + "BaseDefense": 100, + "BaseStamina": 172, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -4355,6 +4361,12 @@ ], "Weight": "50.2 kg", "Height": "1.4 m", + "Previous evolution(s)": [ + { + "Number": "106", + "Name": "Hitmonlee" + } + ], "Special Attack(s)": [ "Brick Break", "Fire Punch", @@ -4362,8 +4374,8 @@ "Thunder Punch" ], "BaseAttack": 138, - "BaseDefense": 204, - "BaseStamina": 100, + "BaseDefense": 100, + "BaseStamina": 204, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -5717,16 +5729,6 @@ "Family": 147, "Name": "Dratini candies" }, - "Next evolution(s)": [ - { - "Number": "148", - "Name": "Dragonair" - }, - { - "Number": "149", - "Name": "Dragonite" - } - ], "Special Attack(s)": [ "Aqua Tail", "Twister", diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index b27eaa388a..ea81b7c093 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1,45 +1,26 @@ import json import os - from pokemongo_bot.base_dir import _base_dir ''' Helper class for updating/retrieving Inventory data ''' - -# -# Abstraction - -class _StaticInventoryComponent(object): +class _BaseInventoryComponent(object): + TYPE = None # base key name for items of this type + ID_FIELD = None # identifier field for items of this type STATIC_DATA_FILE = None # optionally load static data from file, # dropping the data in a static variable named STATIC_DATA - STATIC_DATA = None def __init__(self): + self._data = {} if self.STATIC_DATA_FILE is not None: self.init_static_data() @classmethod def init_static_data(cls): if not hasattr(cls, 'STATIC_DATA') or cls.STATIC_DATA is None: - cls.STATIC_DATA = cls.process_static_data( - json.load(open(cls.STATIC_DATA_FILE))) - - @classmethod - def process_static_data(cls, data): - # optional hook for processing the static data - # default is to use the data directly - return data - - -class _BaseInventoryComponent(_StaticInventoryComponent): - TYPE = None # base key name for items of this type - ID_FIELD = None # identifier field for items of this type - - def __init__(self): - self._data = {} - super(_BaseInventoryComponent, self).__init__() + cls.STATIC_DATA = json.load(open(cls.STATIC_DATA_FILE)) def parse(self, item): # optional hook for parsing the dict for this item @@ -61,22 +42,34 @@ def retrieve_data(self, inventory): def refresh(self, inventory): self._data = self.retrieve_data(inventory) - def get(self, object_id): - return self._data.get(object_id) + def get(self, id): + return self._data.get(id) def all(self): return list(self._data.values()) -# -# Inventory Components +class Candy(object): + def __init__(self, family_id, quantity): + self.type = Pokemons.name_for(family_id) + self.quantity = quantity + + def consume(self, amount): + if self.quantity < amount: + raise Exception('Tried to consume more {} candy than you have'.format(self.type)) + self.quantity -= amount + + def add(self, amount): + if amount < 0: + raise Exception('Must add positive amount of candy') + self.quantity += amount class Candies(_BaseInventoryComponent): TYPE = 'candy' ID_FIELD = 'family_id' @classmethod - def family_id_for(cls, pokemon_id): + def family_id_for(self, pokemon_id): return Pokemons.first_evolution_id_for(pokemon_id) def get(self, pokemon_id): @@ -115,120 +108,17 @@ class Pokemons(_BaseInventoryComponent): ID_FIELD = 'id' STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'pokemon.json') - @classmethod - def process_static_data(cls, data): - pokemon_id = 1 - for poke_info in data: - # prepare types - types = [poke_info['Type I'][0]] # required - for t in poke_info.get('Type II', []): - types.append(t) - poke_info['types'] = types - - # prepare attacks (moves) - cls._process_attacks(poke_info) - cls._process_attacks(poke_info, charged=True) - - # prepare movesets - poke_info['movesets'] = cls._process_movesets(poke_info, pokemon_id) - - # calculate maximum CP for the pokemon (best IVs, lvl 40) - base_attack = poke_info['BaseAttack'] - base_defense = poke_info['BaseDefense'] - base_stamina = poke_info['BaseStamina'] - max_cp = _calc_cp(base_attack, base_defense, base_stamina) - poke_info['max_cp'] = max_cp - - pokemon_id += 1 - return data - - @classmethod - def _process_movesets(cls, poke_info, pokemon_id): - # type: (dict, int) -> List[Moveset] - """ - The optimal moveset is the combination of two moves, one quick move - and one charge move, that deals the most damage over time. - - Because each quick move gains a certain amount of energy (different - for different moves) and each charge move requires a different amount - of energy to use, sometimes, a quick move with lower DPS will be - better since it charges the charge move faster. On the same note, - sometimes a charge move that has lower DPS will be more optimal since - it may require less energy or it may last for a longer period of time. - - Attacker have STAB (Same-type attack bonus - x1.25) pokemon have the - same type as attack. So we add it to the "Combo DPS" of the moveset. - - The defender attacks in intervals of 1 second for the first 2 attacks, - and then in intervals of 2 seconds for the remainder of the attacks. - This explains why we see two consecutive quick attacks at the beginning - of the match. As a result, we add +2 seconds to the DPS calculation - for defender DPS output. - - So to determine an optimal defensive moveset, we follow the same method - as we did for optimal offensive movesets, but instead calculate the - highest "Combo DPS" with an added 2 seconds to the quick move cool down. - - Note: critical hits have not yet been implemented in the game - - See http://pokemongo.gamepress.gg/optimal-moveset-explanation - See http://pokemongo.gamepress.gg/defensive-tactics - """ - - # Prepare movesets - movesets = [] - types = poke_info['types'] - for fm in poke_info['Fast Attack(s)']: - for chm in poke_info['Special Attack(s)']: - movesets.append(Moveset(fm, chm, types, pokemon_id)) - assert len(movesets) > 0 - - # Calculate attack perfection for each moveset - movesets = sorted(movesets, key=lambda m: m.dps_attack) - worst_dps = movesets[0].dps_attack - best_dps = movesets[-1].dps_attack - if best_dps > worst_dps: - for moveset in movesets: - current_dps = moveset.dps_attack - moveset.attack_perfection = \ - (current_dps - worst_dps) / (best_dps - worst_dps) - - # Calculate defense perfection for each moveset - movesets = sorted(movesets, key=lambda m: m.dps_defense) - worst_dps = movesets[0].dps_defense - best_dps = movesets[-1].dps_defense - if best_dps > worst_dps: - for moveset in movesets: - current_dps = moveset.dps_defense - moveset.defense_perfection = \ - (current_dps - worst_dps) / (best_dps - worst_dps) - - return sorted(movesets, key=lambda m: m.dps, reverse=True) - - @classmethod - def _process_attacks(cls, poke_info, charged=False): - # type: (dict, bool) -> List[Attack] - key = 'Fast Attack(s)' if not charged else 'Special Attack(s)' - moves_dict = (ChargedAttacks if charged else FastAttacks).BY_NAME - moves = [] - for name in poke_info[key]: - if name not in moves_dict: - raise KeyError('Unknown {} attack: "{}"'.format( - 'charged' if charged else 'fast', name)) - moves.append(moves_dict[name]) - moves = sorted(moves, key=lambda m: m.dps, reverse=True) - poke_info[key] = moves - assert len(moves) > 0 - return moves + def parse(self, item): + if 'is_egg' in item: + return Egg(item) + return Pokemon(item) @classmethod def data_for(cls, pokemon_id): - # type: (int) -> dict return cls.STATIC_DATA[pokemon_id - 1] @classmethod def name_for(cls, pokemon_id): - # type: (int) -> string return cls.data_for(pokemon_id)['Name'] @classmethod @@ -239,194 +129,24 @@ def first_evolution_id_for(cls, pokemon_id): return pokemon_id @classmethod - def prev_evolution_id_for(cls, pokemon_id): - data = cls.data_for(pokemon_id) - if 'Previous evolution(s)' in data: - return int(data['Previous evolution(s)'][-1]['Number']) - return None - - @classmethod - def next_evolution_ids_for(cls, pokemon_id): + def next_evolution_id_for(cls, pokemon_id): try: - next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] + return int(cls.data_for(pokemon_id)['Next evolution(s)'][0]['Number']) except KeyError: - return [] - # get only next level evolutions, not all possible - ids = [] - for p in next_evolutions: - p_id = int(p['Number']) - if cls.prev_evolution_id_for(p_id) == pokemon_id: - ids.append(p_id) - return ids + return None @classmethod - def last_evolution_ids_for(cls, pokemon_id): + def evolution_cost_for(cls, pokemon_id): try: - next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] + return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) except KeyError: - return [pokemon_id] - # get only final evolutions, not all possible - ids = [] - for p in next_evolutions: - p_id = int(p['Number']) - if len(cls.data_for(p_id).get('Next evolution(s)', [])) == 0: - ids.append(p_id) - assert len(ids) > 0 - return ids - - @classmethod - def has_next_evolution(cls, pokemon_id): - poke_info = cls.data_for(pokemon_id) - return 'Next Evolution Requirements' in poke_info \ - or 'Next evolution(s)' in poke_info - - @classmethod - def evolution_cost_for(cls, pokemon_id): - if not cls.has_next_evolution(pokemon_id): - return None - return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) - - def parse(self, item): - if 'is_egg' in item: - return Egg(item) - return Pokemon(item) + return def all(self): # by default don't include eggs in all pokemon (usually just # makes caller's lives more difficult) return [p for p in super(Pokemons, self).all() if not isinstance(p, Egg)] - -# -# Static Components - -class LevelToCPm(_StaticInventoryComponent): - """ - Data for the CP multipliers at different levels - See http://pokemongo.gamepress.gg/cp-multiplier - See https://github.com/justinleewells/pogo-optimizer/blob/edd692d/data/game/level-to-cpm.json - """ - - STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'level_to_cpm.json') - MAX_LEVEL = 40 - MAX_CPM = .0 - # half of the lowest difference between CPMs - HALF_DIFF_BETWEEN_HALF_LVL = 14e-3 - - @classmethod - def init_static_data(cls): - super(LevelToCPm, cls).init_static_data() - cls.MAX_CPM = cls.cp_multiplier_for(cls.MAX_LEVEL) - - @classmethod - def cp_multiplier_for(cls, level): - # type: (Union[float, int, string]) -> float - level = float(level) - level = str(int(level) if level.is_integer() else level) - return cls.STATIC_DATA[level] - - @classmethod - def level_from_cpm(cls, cp_multiplier): - # type: (float) -> float - for lvl, cpm in cls.STATIC_DATA.iteritems(): - diff = abs(cpm - cp_multiplier) - if diff <= cls.HALF_DIFF_BETWEEN_HALF_LVL: - return float(lvl) - raise ValueError("Unknown cp_multiplier: {}".format(cp_multiplier)) - - -class _Attacks(_StaticInventoryComponent): - BY_NAME = {} # type: Dict[string, Attack] - BY_TYPE = {} # type: Dict[List[Attack]] - BY_DPS = [] # type: List[Attack] - - @classmethod - def process_static_data(cls, moves): - ret = {} - by_type = {} - by_name = {} - fast = cls is FastAttacks - for attack in moves: - attack = Attack(attack) if fast else ChargedAttack(attack) - ret[attack.id] = attack - by_name[attack.name] = attack - - if attack.type not in by_type: - by_type[attack.type] = [] - by_type[attack.type].append(attack) - - for t in by_type.iterkeys(): - attacks = sorted(by_type[t], key=lambda m: m.dps, reverse=True) - min_dps = attacks[-1].dps - max_dps = attacks[0].dps - min_dps - if max_dps > .0: - for attack in attacks: # type: Attack - attack.rate_in_type = (attack.dps - min_dps) / max_dps - by_type[t] = attacks - - cls.BY_NAME = by_name - cls.BY_TYPE = by_type - cls.BY_DPS = sorted(ret.values(), key=lambda m: m.dps, reverse=True) - - return ret - - @classmethod - def data_for(cls, attack_id): - # type: (int) -> Attack - if attack_id not in cls.STATIC_DATA: - raise ValueError("Attack {} not found in {}".format( - attack_id, cls.__name__)) - return cls.STATIC_DATA[attack_id] - - @classmethod - def by_name(cls, name): - # type: (string) -> Attack - return cls.BY_NAME[name] - - @classmethod - def list_for_type(cls, type_name): - # type: (string) -> List[Attack] - """ - :return: Attacks sorted by DPS in descending order - """ - return cls.BY_TYPE[type_name] - - @classmethod - def all(cls): - return cls.STATIC_DATA.values() - - @classmethod - def all_by_dps(cls): - return cls.BY_DPS - - -class FastAttacks(_Attacks): - STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'fast_moves.json') - - -class ChargedAttacks(_Attacks): - STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'charged_moves.json') - - -# -# Instances - -class Candy(object): - def __init__(self, family_id, quantity): - self.type = Pokemons.name_for(family_id) - self.quantity = quantity - - def consume(self, amount): - if self.quantity < amount: - raise Exception('Tried to consume more {} candy than you have'.format(self.type)) - self.quantity -= amount - - def add(self, amount): - if amount < 0: - raise Exception('Must add positive amount of candy') - self.quantity += amount - - class Egg(object): def __init__(self, data): self._data = data @@ -438,297 +158,52 @@ def has_next_evolution(self): class Pokemon(object): def __init__(self, data): self._data = data - # Unique ID for this particular Pokemon self.id = data['id'] - # Id of the such pokemons in pokedex self.pokemon_id = data['pokemon_id'] - - # Combat points value self.cp = data['cp'] - # Base CP multiplier, fixed at the catch time - self.cp_bm = data['cp_multiplier'] - # Changeable part of the CP multiplier, increasing at power up - self.cp_am = data.get('additional_cp_multiplier', .0) - # Resulting CP multiplier - self.cp_m = self.cp_bm + self.cp_am - - # Current pokemon level (half of level is a normal value) - self.level = LevelToCPm.level_from_cpm(self.cp_m) - - # Maximum health points - self.hp_max = data['stamina_max'] - # Current health points - self.hp = data.get('stamina', self.hp_max) - assert 0 <= self.hp <= self.hp_max - - # Individial Values of the current pokemon (different for each pokemon) - self.iv_attack = data.get('individual_attack', 0) - self.iv_defense = data.get('individual_defense', 0) - self.iv_stamina = data.get('individual_stamina', 0) - self._static_data = Pokemons.data_for(self.pokemon_id) self.name = Pokemons.name_for(self.pokemon_id) - self.nickname = data.get('nickname', self.name) - + self.iv = self._compute_iv() self.in_fort = 'deployed_fort_id' in data self.is_favorite = data.get('favorite', 0) is 1 - # Basic Values of the current pokemon (identical for all such pokemons) - self.base_attack = self._static_data['BaseAttack'] - self.base_defense = self._static_data['BaseDefense'] - self.base_stamina = self._static_data['BaseStamina'] - - # Maximum possible CP for the current pokemon - self.max_cp = self._static_data['max_cp'] - - self.fast_attack = FastAttacks.data_for(data['move_1']) - self.charged_attack = ChargedAttacks.data_for(data['move_2']) - - # Internal values (IV) perfection percent - self.iv = self._compute_iv_perfection() - - # IV CP perfection - kind of IV perfection percent but calculated - # using weight of each IV in its contribution to CP of the best - # evolution of current pokemon - # So it tends to be more accurate than simple IV perfection - self.ivcp = self._compute_cp_perfection() - - # Exact value of current CP (not rounded) - self.cp_exact = _calc_cp( - self.base_attack, self.base_defense, self.base_stamina, - self.iv_attack, self.iv_defense, self.iv_stamina, self.cp_m) - - # Percent of maximum possible CP - self.cp_percent = self.cp_exact / self.max_cp - - # Get moveset instance with calculated DPS and perfection percents - self.moveset = self._get_moveset() - - def __str__(self): - return self.name - - def __repr__(self): - return self.name - def can_evolve_now(self): - return self.has_next_evolution() and \ - self.candy_quantity >= self.evolution_cost + return self.has_next_evolution() and self.candy_quantity >= self.evolution_cost def has_next_evolution(self): - return Pokemons.has_next_evolution(self.pokemon_id) + return 'Next Evolution Requirements' in self._static_data def has_seen_next_evolution(self): - for pokemon_id in self.next_evolution_ids: - if pokedex().captured(pokemon_id): - return True - return False + return pokedex().captured(self.next_evolution_id) @property - def family_id(self): - return self.first_evolution_id + def next_evolution_id(self): + return Pokemons.next_evolution_id_for(self.pokemon_id) @property def first_evolution_id(self): return Pokemons.first_evolution_id_for(self.pokemon_id) - @property - def prev_evolution_id(self): - return Pokemons.prev_evolution_id_for(self.pokemon_id) - - @property - def next_evolution_ids(self): - return Pokemons.next_evolution_ids_for(self.pokemon_id) - - @property - def last_evolution_ids(self): - return Pokemons.last_evolution_ids_for(self.pokemon_id) - @property def candy_quantity(self): return candies().get(self.pokemon_id).quantity @property def evolution_cost(self): - return Pokemons.evolution_cost_for(self.pokemon_id) - - def _compute_iv_perfection(self): - total_iv = self.iv_attack + self.iv_defense + self.iv_stamina - iv_perfection = round((total_iv / 45.0), 2) - return iv_perfection - - def _compute_cp_perfection(self): - """ - CP perfect percent is more accurate than IV perfect - - We know attack plays an important role in CP, and different - pokemons have different base value, that's means 15/14/15 is - better than 14/15/15 for lot of pokemons, and if one pokemon's - base def is more than base sta, 15/15/14 is better than 15/14/15. - - See https://github.com/jabbink/PokemonGoBot/issues/469 - - So calculate CP perfection at final level for the best of the final - evolutions of the pokemon. - """ - variants = [] - iv_attack = self.iv_attack - iv_defense = self.iv_defense - iv_stamina = self.iv_stamina - cp_m = LevelToCPm.MAX_CPM - last_evolution_ids = self.last_evolution_ids - for pokemon_id in last_evolution_ids: - poke_info = Pokemons.data_for(pokemon_id) - base_attack = poke_info['BaseAttack'] - base_defense = poke_info['BaseDefense'] - base_stamina = poke_info['BaseStamina'] - - # calculate CP variants at maximum level - worst_cp = _calc_cp(base_attack, base_defense, base_stamina, - 0, 0, 0, cp_m) - perfect_cp = _calc_cp(base_attack, base_defense, base_stamina, - cp_multiplier=cp_m) - current_cp = _calc_cp(base_attack, base_defense, base_stamina, - iv_attack, iv_defense, iv_stamina, cp_m) - cp_perfection = (current_cp - worst_cp) / (perfect_cp - worst_cp) - variants.append(cp_perfection) - - # get best value (probably for the best evolution) - cp_perfection = max(variants) - return cp_perfection - - def _get_moveset(self): - move1 = self.fast_attack - move2 = self.charged_attack - movesets = self._static_data['movesets'] - current_moveset = None - for moveset in movesets: # type: Moveset - if moveset.fast_attack == move1 and moveset.charged_attack == move2: - current_moveset = moveset - break - - if current_moveset is None: - raise Exception("Unexpected moveset [{}, {}] for #{} {}".format( - move1, move2, self.pokemon_id, self.name)) - - return current_moveset - - -class Attack(object): - def __init__(self, data): - # self._data = data # Not needed - all saved in fields - self.id = data['id'] - self.name = data['name'] - self.type = data['type'] - self.damage = data['damage'] - self.duration = data['duration'] / 1000.0 # duration in seconds - - # Energy addition for fast attack - # Energy cost for charged attack - self.energy = data['energy'] - - # Damage Per Second - # recalc for better precision - self.dps = self.damage / self.duration - - # Perfection of the attack in it's type (from 0 to 1) - self.rate_in_type = .0 - - @property - def damage_with_stab(self): - # damage with STAB (Same-type attack bonus) - return self.damage * STAB_FACTOR - - @property - def dps_with_stab(self): - # DPS with STAB (Same-type attack bonus) - return self.dps * STAB_FACTOR - - @property - def energy_per_second(self): - return self.energy / self.duration - - @property - def dodge_window(self): - # TODO: Attack Dodge Window - return NotImplemented - - @property - def is_charged(self): - return False - - def __str__(self): - return self.name - - def __repr__(self): - return self.name - - -class ChargedAttack(Attack): - def __init__(self, data): - super(ChargedAttack, self).__init__(data) - - @property - def is_charged(self): - return True - - -class Moveset(object): - def __init__(self, fm, chm, pokemon_types=(), pokemon_id=-1): - # type: (Attack, ChargedAttack, List[string], int) -> None - self.pokemon_id = pokemon_id - self.fast_attack = fm - self.charged_attack = chm - - # See Pokemons._process_movesets() - # See http://pokemongo.gamepress.gg/optimal-moveset-explanation - # See http://pokemongo.gamepress.gg/defensive-tactics - - fm_number = 100 # for simplicity we use 100 - - fm_energy = fm.energy * fm_number - fm_damage = fm.damage * fm_number - fm_secs = fm.duration * fm_number - - # Defender attacks in intervals of 1 second for the - # first 2 attacks, and then in intervals of 2 seconds - # So add 1.95 seconds to the quick move cool down for defense - # 1.95 is something like an average here - # TODO: Do something better? - fm_defense_secs = (fm.duration + 1.95) * fm_number - - chm_number = fm_energy / chm.energy - chm_damage = chm.damage * chm_number - chm_secs = chm.duration * chm_number - - damage_sum = fm_damage + chm_damage - # raw Damage-Per-Second for the moveset - self.dps = damage_sum / (fm_secs + chm_secs) - # average DPS for defense - self.dps_defense = damage_sum / (fm_defense_secs + chm_secs) - - # apply STAB (Same-type attack bonus) - if fm.type in pokemon_types: - fm_damage *= STAB_FACTOR - if chm.type in pokemon_types: - chm_damage *= STAB_FACTOR - - # DPS for attack (counting STAB) - self.dps_attack = (fm_damage + chm_damage) / (fm_secs + chm_secs) + return self._static_data['Next Evolution Requirements']['Amount'] - # Moveset perfection percent attack and for defense - # Calculated for current pokemon, not between all pokemons - # So 100% perfect moveset can be weak if pokemon is weak (e.g. Caterpie) - self.attack_perfection = .0 - self.defense_perfection = .0 + def _compute_iv(self): + total_IV = 0.0 + iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - # TODO: True DPS for real combat (floor(Attack/200 * MovePower * STAB) + 1) - # See http://pokemongo.gamepress.gg/pokemon-attack-explanation - - def __str__(self): - return '[{}, {}]'.format(self.fast_attack, self.charged_attack) - - def __repr__(self): - return '[{}, {}]'.format(self.fast_attack, self.charged_attack) + for individual_stat in iv_stats: + try: + total_IV += self._data[individual_stat] + except Exception: + self._data[individual_stat] = 0 + continue + pokemon_potential = round((total_IV / 45.0), 2) + return pokemon_potential class Inventory(object): @@ -752,47 +227,8 @@ def refresh(self): with open(user_web_inventory, 'w') as outfile: json.dump(inventory, outfile) -# -# Usage helpers - -# STAB (Same-type attack bonus) -STAB_FACTOR = 1.25 _inventory = None -LevelToCPm() # init LevelToCPm -FastAttacks() # init FastAttacks -ChargedAttacks() # init ChargedAttacks - - -def _calc_cp(base_attack, base_defense, base_stamina, - iv_attack=15, iv_defense=15, iv_stamina=15, - cp_multiplier=LevelToCPm.MAX_CPM): - """ - CP calculation - - CP = (Attack * Defense^0.5 * Stamina^0.5 * CP_Multiplier^2) / 10 - CP = (BaseAtk+AtkIV) * (BaseDef+DefIV)^0.5 * (BaseStam+StamIV)^0.5 * Lvl(CPScalar)^2 / 10 - - See https://www.reddit.com/r/TheSilphRoad/comments/4t7r4d/exact_pokemon_cp_formula/ - See https://www.reddit.com/r/pokemongodev/comments/4t7xb4/exact_cp_formula_from_stats_and_cpm_and_an_update/ - See http://pokemongo.gamepress.gg/pokemon-stats-advanced - See http://pokemongo.gamepress.gg/cp-multiplier - See http://gaming.stackexchange.com/questions/280491/formula-to-calculate-pokemon-go-cp-and-hp - - :param base_attack: Pokemon BaseAttack - :param base_defense: Pokemon BaseDefense - :param base_stamina: Pokemon BaseStamina - :param iv_attack: Pokemon IndividualAttack (0..15) - :param iv_defense: Pokemon IndividualDefense (0..15) - :param iv_stamina: Pokemon IndividualStamina (0..15) - :param cp_multiplier: CP Multiplier (0.79030001 is max - value for level 40) - :return: CP as float - """ - return (base_attack + iv_attack) \ - * ((base_defense + iv_defense)**0.5) \ - * ((base_stamina + iv_stamina)**0.5) \ - * (cp_multiplier ** 2) / 10 - def init_inventory(bot): global _inventory @@ -821,15 +257,3 @@ def pokemons(refresh=False): def items(): return _inventory.items - - -def levels_to_cpm(): - return LevelToCPm - - -def fast_attacks(): - return FastAttacks - - -def charged_attacks(): - return ChargedAttacks diff --git a/tests/inventory_test.py b/tests/inventory_test.py deleted file mode 100644 index 8362ce2b91..0000000000 --- a/tests/inventory_test.py +++ /dev/null @@ -1,183 +0,0 @@ -import unittest - -from pokemongo_bot.inventory import * - - -class InventoryTest(unittest.TestCase): - def test_pokemons(self): - # Init data - self.assertEqual(len(Pokemons().all()), 0) # No inventory loaded here - - obj = Pokemons - self.assertEqual(len(obj.STATIC_DATA), 151) - - for poke_info in obj.STATIC_DATA: - name = poke_info['Name'] - pokemon_id = int(poke_info['Number']) - self.assertTrue(1 <= pokemon_id <= 151) - - self.assertGreaterEqual(len(poke_info['movesets']), 1) - self.assertTrue(262 <= poke_info['max_cp'] <= 4145) - self.assertTrue(1 <= len(poke_info['types']) <= 2) - self.assertTrue(40 <= poke_info['BaseAttack'] <= 284) - self.assertTrue(20 <= poke_info['BaseDefense'] <= 500) - self.assertTrue(54 <= poke_info['BaseStamina'] <= 242) - self.assertTrue(.0 <= poke_info['CaptureRate'] <= .56) - self.assertTrue(.0 <= poke_info['FleeRate'] <= .99) - self.assertTrue(1 <= len(poke_info['Weaknesses']) <= 7) - self.assertTrue(3 <= len(name) <= 10) - - self.assertGreaterEqual(len(poke_info['Classification']), 11) - self.assertGreaterEqual(len(poke_info['Fast Attack(s)']), 1) - self.assertGreaterEqual(len(poke_info['Special Attack(s)']), 1) - - self.assertIs(obj.data_for(pokemon_id), poke_info) - self.assertIs(obj.name_for(pokemon_id), name) - - first_evolution_id = obj.first_evolution_id_for(pokemon_id) - self.assertGreaterEqual(first_evolution_id, 1) - next_evolution_ids = obj.next_evolution_ids_for(pokemon_id) - last_evolution_ids = obj.last_evolution_ids_for(pokemon_id) - candies_cost = obj.evolution_cost_for(pokemon_id) - obj.prev_evolution_id_for(pokemon_id) # just call test - self.assertGreaterEqual(len(last_evolution_ids), 1) - - if not obj.has_next_evolution(pokemon_id): - assert 'Next evolution(s)' not in poke_info - assert 'Next Evolution Requirements' not in poke_info - else: - self.assertGreaterEqual(len(next_evolution_ids), 1) - self.assertLessEqual(len(next_evolution_ids), len(last_evolution_ids)) - - reqs = poke_info['Next Evolution Requirements'] - self.assertEqual(reqs["Family"], first_evolution_id) - candies_name = obj.name_for(first_evolution_id) + ' candies' - self.assertEqual(reqs["Name"], candies_name) - self.assertIsNotNone(candies_cost) - self.assertTrue(12 <= candies_cost <= 400) - self.assertEqual(reqs["Amount"], candies_cost) - - evolutions = poke_info["Next evolution(s)"] - self.assertGreaterEqual(len(evolutions), len(next_evolution_ids)) - - for p in evolutions: - p_id = int(p["Number"]) - self.assertNotEqual(p_id, pokemon_id) - self.assertEqual(p["Name"], obj.name_for(p_id)) - - for p_id in next_evolution_ids: - self.assertEqual(obj.prev_evolution_id_for(p_id), pokemon_id) - prev_evs = obj.data_for(p_id)["Previous evolution(s)"] - self.assertGreaterEqual(len(prev_evs), 1) - self.assertEqual(int(prev_evs[-1]["Number"]), pokemon_id) - self.assertEqual(prev_evs[-1]["Name"], name) - - # Only Eevee has 3 next evolutions - self.assertEqual(len(next_evolution_ids), - 1 if pokemon_id != 133 else 3) - - if "Previous evolution(s)" in poke_info: - for p in poke_info["Previous evolution(s)"]: - p_id = int(p["Number"]) - self.assertNotEqual(p_id, pokemon_id) - self.assertEqual(p["Name"], obj.name_for(p_id)) - - # - # Specific pokemons testing - - poke = Pokemon({ - "num_upgrades": 2, "move_1": 210, "move_2": 69, "pokeball": 2, - "favorite": 1, "pokemon_id": 42, "battles_attacked": 4, - "stamina": 76, "stamina_max": 76, "individual_attack": 9, - "individual_defense": 4, "individual_stamina": 8, - "cp_multiplier": 0.4627983868122101, - "additional_cp_multiplier": 0.018886566162109375, - "cp": 653, "nickname": "Golb", "id": 13632861873471324}) - self.assertEqual(poke.level, 12.5) - self.assertEqual(poke.iv, 0.47) - self.assertAlmostEqual(poke.ivcp, 0.482845351) - self.assertAlmostEqual(poke.max_cp, 1921.34561459) - self.assertAlmostEqual(poke.cp_percent, 0.34000973) - self.assertTrue(poke.is_favorite) - self.assertEqual(poke.name, 'Golbat') - self.assertEqual(poke.nickname, "Golb") - self.assertAlmostEqual(poke.moveset.dps, 10.7540173053) - self.assertAlmostEqual(poke.moveset.dps_attack, 12.14462299) - self.assertAlmostEqual(poke.moveset.dps_defense, 4.876681614) - self.assertAlmostEqual(poke.moveset.attack_perfection, 0.4720730048) - self.assertAlmostEqual(poke.moveset.defense_perfection, 0.8158081497) - - poke = Pokemon({ - "move_1": 221, "move_2": 129, "pokemon_id": 19, "cp": 110, - "individual_attack": 6, "stamina_max": 22, "individual_defense": 14, - "cp_multiplier": 0.37523558735847473, "id": 7841053399}) - self.assertEqual(poke.level, 7.5) - self.assertEqual(poke.iv, 0.44) - self.assertAlmostEqual(poke.ivcp, 0.452398293) - self.assertAlmostEqual(poke.max_cp, 581.64643575) - self.assertAlmostEqual(poke.cp_percent, 0.189251848608) - self.assertFalse(poke.is_favorite) - self.assertEqual(poke.name, 'Rattata') - self.assertEqual(poke.nickname, 'Rattata') - self.assertAlmostEqual(poke.moveset.dps, 12.5567813108) - self.assertAlmostEqual(poke.moveset.dps_attack, 15.6959766385) - self.assertAlmostEqual(poke.moveset.dps_defense, 5.54282440561) - self.assertAlmostEqual(poke.moveset.attack_perfection, 0.835172881385) - self.assertAlmostEqual(poke.moveset.defense_perfection, 0.603137650999) - - def test_levels_to_cpm(self): - l2c = LevelToCPm - self.assertIs(levels_to_cpm(), l2c) - max_cpm = l2c.cp_multiplier_for(l2c.MAX_LEVEL) - self.assertEqual(l2c.MAX_LEVEL, 40) - self.assertEqual(l2c.MAX_CPM, max_cpm) - self.assertEqual(len(l2c.STATIC_DATA), 79) - - self.assertEqual(l2c.cp_multiplier_for("1"), 0.094) - self.assertEqual(l2c.cp_multiplier_for(1), 0.094) - self.assertEqual(l2c.cp_multiplier_for(1.0), 0.094) - self.assertEqual(l2c.cp_multiplier_for("17.5"), 0.558830576) - self.assertEqual(l2c.cp_multiplier_for(17.5), 0.558830576) - self.assertEqual(l2c.cp_multiplier_for('40.0'), 0.79030001) - self.assertEqual(l2c.cp_multiplier_for(40.0), 0.79030001) - self.assertEqual(l2c.cp_multiplier_for(40), 0.79030001) - - self.assertEqual(l2c.level_from_cpm(0.79030001), 40.0) - self.assertEqual(l2c.level_from_cpm(0.7903), 40.0) - - def test_attacks(self): - self._test_attacks(fast_attacks, FastAttacks) - self._test_attacks(charged_attacks, ChargedAttacks) - - def _test_attacks(self, callback, clazz): - charged = clazz is ChargedAttacks - self.assertIs(callback(), clazz) - - # check consistency - attacks = clazz.all_by_dps() - number = len(attacks) - self.assertTrue(number > 0) - self.assertGreaterEqual(len(clazz.BY_TYPE), 17) - self.assertEqual(number, len(clazz.all())) - self.assertEqual(number, len(clazz.STATIC_DATA)) - self.assertEqual(number, len(clazz.BY_NAME)) - self.assertEqual(number, sum([len(l) for l in clazz.BY_TYPE.values()])) - - # check data - prev_dps = float("inf") - for attack in attacks: # type: Attack - self.assertGreater(attack.id, 0) - self.assertGreater(len(attack.name), 0) - self.assertGreater(len(attack.type), 0) - self.assertGreaterEqual(attack.damage, 0) - self.assertGreater(attack.duration, .0) - self.assertGreater(attack.energy, 0) - self.assertGreaterEqual(attack.dps, 0) - self.assertTrue(.0 <= attack.rate_in_type <= 1.0) - self.assertLessEqual(attack.dps, prev_dps) - self.assertEqual(attack.is_charged, charged) - self.assertIs(attack, clazz.data_for(attack.id)) - self.assertIs(attack, clazz.by_name(attack.name)) - self.assertTrue(attack in clazz.BY_TYPE[attack.type]) - self.assertIsInstance(attack, ChargedAttack if charged else Attack) - prev_dps = attack.dps From 9ccadcd3af4da0cc0039d3552d26bba05e78ad99 Mon Sep 17 00:00:00 2001 From: sia84 Date: Thu, 11 Aug 2016 00:08:41 -0400 Subject: [PATCH 093/143] run.bat for windows (#3542) run.bat with persistent loop --- run.bat | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 run.bat diff --git a/run.bat b/run.bat new file mode 100644 index 0000000000..047c8f6fd4 --- /dev/null +++ b/run.bat @@ -0,0 +1,10 @@ +@echo off +set /a x=0 +:LOOP +echo Running pokecli.py for count: %x% +REM Change the path for python.exe if it's different for you +C:\Python27\python.exe pokecli.py +REM Waits for 60 seconds +ping 127.0.0.1 -n 60 > nul +set /a x+=1 +goto :LOOP From 847154d80805377a9c638871b3317b26ae6e0b4c Mon Sep 17 00:00:00 2001 From: Dmitry Ovodov Date: Thu, 11 Aug 2016 11:09:23 +0700 Subject: [PATCH 094/143] Fix error when MoveToFort called from handle_soft_ban.py (#3500) * Fix error when MoveToFort called from handle_soft_ban.py * Added myself to CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + pokemongo_bot/cell_workers/move_to_fort.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index a963396fcb..fc2478f20c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -61,3 +61,4 @@ * JaapMoolenaar * eevee-github * g0vanish + * cmezh diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 2be287e86c..24ecf5e74a 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -13,9 +13,13 @@ class MoveToFort(BaseTask): def initialize(self): self.lure_distance = 0 - self.lure_attraction = self.config.get("lure_attraction", True) - self.lure_max_distance = self.config.get("lure_max_distance", 2000) - self.ignore_item_count = self.config.get("ignore_item_count", False) + if self.config: + self.lure_attraction = self.config.get("lure_attraction", True) + self.lure_max_distance = self.config.get("lure_max_distance", 2000) + self.ignore_item_count = self.config.get("ignore_item_count", False) + else: + self.lure_attraction = None + self.ignore_item_count = True def should_run(self): has_space_for_loot = self.bot.has_space_for_loot() From e2e32880deb32735b2811f1b10fe0997d1ec802a Mon Sep 17 00:00:00 2001 From: Amal Samally Date: Thu, 11 Aug 2016 08:35:25 +0400 Subject: [PATCH 095/143] Fixed list of charged attacks for Venonat (#3548) + BaseDefense/BaseStamina info fix (#3550) * Revert "Revert "Better inventory: attacks & movesets, IV CP perfection, pokemon level, etc.." (#3549)" This reverts commit e9b229ec0fd14a4814ea7431b1256850e907cfbf. * Fix BaseDefense/BaseStamina and type info Fixed BaseDefense/BaseStamina info (was mixed up) Fixed type info for Mr. Mime, Clefairy, Clefable, Jigglypuff, Wigglytuff Added check for correctness of exact CP value calculation * Fixed list of charged attacks for Venonat * Don't kill bot in case of unexpected moveset, use fallback + better error message --- .gitignore | 1 + data/charged_moves.json | 3 +- data/level_to_cpm.json | 81 +++++ data/pokemon.json | 632 +++++++++++++++++----------------- pokemongo_bot/inventory.py | 686 ++++++++++++++++++++++++++++++++++--- tests/inventory_test.py | 183 ++++++++++ 6 files changed, 1221 insertions(+), 365 deletions(-) create mode 100644 data/level_to_cpm.json create mode 100644 tests/inventory_test.py diff --git a/.gitignore b/.gitignore index 4721ce0253..06973c1249 100644 --- a/.gitignore +++ b/.gitignore @@ -100,6 +100,7 @@ share/ # PyCharm IDE settings .idea/ +*.iml # Personal load details src/ diff --git a/data/charged_moves.json b/data/charged_moves.json index 3d487b7201..c3f993191c 100644 --- a/data/charged_moves.json +++ b/data/charged_moves.json @@ -84,9 +84,8 @@ {"id":101,"name":"Flame Charge","type":"Fire","damage":25,"duration":3100,"energy":20,"dps":8.06}, {"id":34,"name":"Heart Stamp","type":"Psychic","damage":20,"duration":2550,"energy":25,"dps":7.84}, {"id":75,"name":"Parabolic Charge","type":"Electric","damage":15,"duration":2100,"energy":20,"dps":7.14}, -{"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":3700,"energy":20,"dps":6.75}, {"id":111,"name":"Icy Wind","type":"Ice","damage":25,"duration":3800,"energy":20,"dps":6.57}, {"id":84,"name":"Disarming Voice","type":"Fairy","damage":25,"duration":3900,"energy":20,"dps":6.41}, {"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":4000,"energy":20,"dps":6.25}, {"id":66,"name":"Shadow Sneak","type":"Ghost","damage":15,"duration":3100,"energy":20,"dps":4.83}, -{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] \ No newline at end of file +{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] diff --git a/data/level_to_cpm.json b/data/level_to_cpm.json new file mode 100644 index 0000000000..d2483d9a41 --- /dev/null +++ b/data/level_to_cpm.json @@ -0,0 +1,81 @@ +{ + "1": 0.094, + "1.5": 0.135137432, + "2": 0.16639787, + "2.5": 0.192650919, + "3": 0.21573247, + "3.5": 0.236572661, + "4": 0.25572005, + "4.5": 0.273530381, + "5": 0.29024988, + "5.5": 0.306057377, + "6": 0.3210876, + "6.5": 0.335445036, + "7": 0.34921268, + "7.5": 0.362457751, + "8": 0.37523559, + "8.5": 0.387592406, + "9": 0.39956728, + "9.5": 0.411193551, + "10": 0.42250001, + "10.5": 0.432926419, + "11": 0.44310755, + "11.5": 0.4530599578, + "12": 0.46279839, + "12.5": 0.472336083, + "13": 0.48168495, + "13.5": 0.4908558, + "14": 0.49985844, + "14.5": 0.508701765, + "15": 0.51739395, + "15.5": 0.525942511, + "16": 0.53435433, + "16.5": 0.542635767, + "17": 0.55079269, + "17.5": 0.558830576, + "18": 0.56675452, + "18.5": 0.574569153, + "19": 0.58227891, + "19.5": 0.589887917, + "20": 0.59740001, + "20.5": 0.604818814, + "21": 0.61215729, + "21.5": 0.619399365, + "22": 0.62656713, + "22.5": 0.633644533, + "23": 0.64065295, + "23.5": 0.647576426, + "24": 0.65443563, + "24.5": 0.661214806, + "25": 0.667934, + "25.5": 0.674577537, + "26": 0.68116492, + "26.5": 0.687680648, + "27": 0.69414365, + "27.5": 0.700538673, + "28": 0.70688421, + "28.5": 0.713164996, + "29": 0.71939909, + "29.5": 0.725571552, + "30": 0.7317, + "30.5": 0.734741009, + "31": 0.73776948, + "31.5": 0.740785574, + "32": 0.74378943, + "32.5": 0.746781211, + "33": 0.74976104, + "33.5": 0.752729087, + "34": 0.75568551, + "34.5": 0.758630378, + "35": 0.76156384, + "35.5": 0.764486065, + "36": 0.76739717, + "36.5": 0.770297266, + "37": 0.7731865, + "37.5": 0.776064962, + "38": 0.77893275, + "38.5": 0.781790055, + "39": 0.78463697, + "39.5": 0.787473578, + "40": 0.79030001 +} \ No newline at end of file diff --git a/data/pokemon.json b/data/pokemon.json index e64b7c31fb..44a22a9fd0 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -42,8 +42,8 @@ "Sludge Bomb" ], "BaseAttack": 126, - "BaseDefense": 90, - "BaseStamina": 126, + "BaseDefense": 126, + "BaseStamina": 90, "CaptureRate": 0.16, "FleeRate": 0.1 }, @@ -92,8 +92,8 @@ "Solar Beam" ], "BaseAttack": 156, - "BaseDefense": 120, - "BaseStamina": 158, + "BaseDefense": 158, + "BaseStamina": 120, "CaptureRate": 0.08, "FleeRate": 0.07 }, @@ -135,8 +135,8 @@ "Solar Beam" ], "BaseAttack": 198, - "BaseDefense": 160, - "BaseStamina": 200, + "BaseDefense": 200, + "BaseStamina": 160, "CaptureRate": 0.04, "FleeRate": 0.05 }, @@ -179,8 +179,8 @@ "Flamethrower" ], "BaseAttack": 128, - "BaseDefense": 78, - "BaseStamina": 108, + "BaseDefense": 108, + "BaseStamina": 78, "CaptureRate": 0.16, "FleeRate": 0.1 }, @@ -225,8 +225,8 @@ "Flamethrower" ], "BaseAttack": 160, - "BaseDefense": 116, - "BaseStamina": 140, + "BaseDefense": 140, + "BaseStamina": 116, "CaptureRate": 0.08, "FleeRate": 0.07 }, @@ -267,8 +267,8 @@ "Flamethrower" ], "BaseAttack": 212, - "BaseDefense": 156, - "BaseStamina": 182, + "BaseDefense": 182, + "BaseStamina": 156, "CaptureRate": 0.04, "FleeRate": 0.05 }, @@ -310,8 +310,8 @@ "Water Pulse" ], "BaseAttack": 112, - "BaseDefense": 88, - "BaseStamina": 142, + "BaseDefense": 142, + "BaseStamina": 88, "CaptureRate": 0.16, "FleeRate": 0.1 }, @@ -355,8 +355,8 @@ "Ice Beam" ], "BaseAttack": 144, - "BaseDefense": 118, - "BaseStamina": 176, + "BaseDefense": 176, + "BaseStamina": 118, "CaptureRate": 0.08, "FleeRate": 0.07 }, @@ -393,8 +393,8 @@ "Ice Beam" ], "BaseAttack": 186, - "BaseDefense": 158, - "BaseStamina": 222, + "BaseDefense": 222, + "BaseStamina": 158, "CaptureRate": 0.04, "FleeRate": 0.05 }, @@ -435,8 +435,8 @@ "Struggle" ], "BaseAttack": 62, - "BaseDefense": 90, - "BaseStamina": 66, + "BaseDefense": 66, + "BaseStamina": 90, "CaptureRate": 0.4, "FleeRate": 0.2 }, @@ -479,8 +479,8 @@ "Struggle" ], "BaseAttack": 56, - "BaseDefense": 100, - "BaseStamina": 86, + "BaseDefense": 86, + "BaseStamina": 100, "CaptureRate": 0.2, "FleeRate": 0.09 }, @@ -523,8 +523,8 @@ "Signal Beam" ], "BaseAttack": 144, - "BaseDefense": 120, - "BaseStamina": 144, + "BaseDefense": 144, + "BaseStamina": 120, "CaptureRate": 0.1, "FleeRate": 0.06 }, @@ -569,8 +569,8 @@ "Struggle" ], "BaseAttack": 68, - "BaseDefense": 80, - "BaseStamina": 64, + "BaseDefense": 64, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.2 }, @@ -617,8 +617,8 @@ "Struggle" ], "BaseAttack": 62, - "BaseDefense": 90, - "BaseStamina": 82, + "BaseDefense": 82, + "BaseStamina": 90, "CaptureRate": 0.2, "FleeRate": 0.09 }, @@ -706,8 +706,8 @@ } ], "BaseAttack": 94, - "BaseDefense": 80, - "BaseStamina": 90, + "BaseDefense": 90, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.2 }, @@ -754,8 +754,8 @@ } ], "BaseAttack": 126, - "BaseDefense": 126, - "BaseStamina": 122, + "BaseDefense": 122, + "BaseStamina": 126, "CaptureRate": 0.2, "FleeRate": 0.09 }, @@ -833,8 +833,8 @@ } ], "BaseAttack": 92, - "BaseDefense": 60, - "BaseStamina": 86, + "BaseDefense": 86, + "BaseStamina": 60, "CaptureRate": 0.4, "FleeRate": 0.2 }, @@ -866,8 +866,8 @@ } ], "BaseAttack": 146, - "BaseDefense": 110, - "BaseStamina": 150, + "BaseDefense": 150, + "BaseStamina": 110, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -908,8 +908,8 @@ "Twister" ], "BaseAttack": 102, - "BaseDefense": 80, - "BaseStamina": 78, + "BaseDefense": 78, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -945,8 +945,8 @@ "Twister" ], "BaseAttack": 168, - "BaseDefense": 130, - "BaseStamina": 146, + "BaseDefense": 146, + "BaseStamina": 130, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -984,8 +984,8 @@ "Wrap" ], "BaseAttack": 112, - "BaseDefense": 70, - "BaseStamina": 112, + "BaseDefense": 112, + "BaseStamina": 70, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -1018,8 +1018,8 @@ "Sludge Wave" ], "BaseAttack": 166, - "BaseDefense": 120, - "BaseStamina": 166, + "BaseDefense": 166, + "BaseStamina": 120, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -1056,8 +1056,8 @@ "Thunderbolt" ], "BaseAttack": 124, - "BaseDefense": 70, - "BaseStamina": 108, + "BaseDefense": 108, + "BaseStamina": 70, "CaptureRate": 0.16, "FleeRate": 0.1 }, @@ -1089,8 +1089,8 @@ "Thunder Punch" ], "BaseAttack": 200, - "BaseDefense": 120, - "BaseStamina": 154, + "BaseDefense": 154, + "BaseStamina": 120, "CaptureRate": 0.08, "FleeRate": 0.06 }, @@ -1129,8 +1129,8 @@ "Rock Tomb" ], "BaseAttack": 90, - "BaseDefense": 100, - "BaseStamina": 114, + "BaseDefense": 114, + "BaseStamina": 100, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -1164,8 +1164,8 @@ "Rock Tomb" ], "BaseAttack": 150, - "BaseDefense": 150, - "BaseStamina": 172, + "BaseDefense": 172, + "BaseStamina": 150, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -1207,8 +1207,8 @@ "Sludge Bomb" ], "BaseAttack": 100, - "BaseDefense": 110, - "BaseStamina": 104, + "BaseDefense": 104, + "BaseStamina": 110, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -1232,7 +1232,7 @@ "Previous evolution(s)": [ { "Number": "029", - "Name": "Nidoran F" + "Name": "Nidoran F" } ], "Next Evolution Requirements": { @@ -1252,8 +1252,8 @@ "Sludge Bomb" ], "BaseAttack": 132, - "BaseDefense": 140, - "BaseStamina": 136, + "BaseDefense": 136, + "BaseStamina": 140, "CaptureRate": 0.2, "FleeRate": 0.07 }, @@ -1295,8 +1295,8 @@ "Stone Edge" ], "BaseAttack": 184, - "BaseDefense": 180, - "BaseStamina": 190, + "BaseDefense": 190, + "BaseStamina": 180, "CaptureRate": 0.1, "FleeRate": 0.05 }, @@ -1338,8 +1338,8 @@ "Sludge Bomb" ], "BaseAttack": 110, - "BaseDefense": 92, - "BaseStamina": 94, + "BaseDefense": 94, + "BaseStamina": 92, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -1383,8 +1383,8 @@ "Sludge Bomb" ], "BaseAttack": 142, - "BaseDefense": 122, - "BaseStamina": 128, + "BaseDefense": 128, + "BaseStamina": 122, "CaptureRate": 0.2, "FleeRate": 0.07 }, @@ -1426,8 +1426,8 @@ "Sludge Wave" ], "BaseAttack": 204, - "BaseDefense": 162, - "BaseStamina": 170, + "BaseDefense": 170, + "BaseStamina": 162, "CaptureRate": 0.1, "FleeRate": 0.05 }, @@ -1436,7 +1436,7 @@ "Name": "Clefairy", "Classification": "Fairy Pokemon", "Type I": [ - "Normal" + "Fairy" ], "Weaknesses": [ "Fighting" @@ -1464,8 +1464,8 @@ "Moonblast" ], "BaseAttack": 116, - "BaseDefense": 140, - "BaseStamina": 124, + "BaseDefense": 124, + "BaseStamina": 140, "CaptureRate": 0.24, "FleeRate": 0.1 }, @@ -1474,7 +1474,7 @@ "Name": "Clefable", "Classification": "Fairy Pokemon", "Type I": [ - "Normal" + "Fairy" ], "Weaknesses": [ "Fighting" @@ -1497,8 +1497,8 @@ "Psychic" ], "BaseAttack": 178, - "BaseDefense": 190, - "BaseStamina": 178, + "BaseDefense": 178, + "BaseStamina": 190, "CaptureRate": 0.08, "FleeRate": 0.06 }, @@ -1537,8 +1537,8 @@ "Flamethrower" ], "BaseAttack": 106, - "BaseDefense": 76, - "BaseStamina": 118, + "BaseDefense": 118, + "BaseStamina": 76, "CaptureRate": 0.24, "FleeRate": 0.1 }, @@ -1572,8 +1572,8 @@ "Heat Wave" ], "BaseAttack": 176, - "BaseDefense": 146, - "BaseStamina": 194, + "BaseDefense": 194, + "BaseStamina": 146, "CaptureRate": 0.08, "FleeRate": 0.06 }, @@ -1584,6 +1584,9 @@ "Type I": [ "Normal" ], + "Type II": [ + "Fairy" + ], "Weaknesses": [ "Fighting" ], @@ -1610,8 +1613,8 @@ "Play Rough" ], "BaseAttack": 98, - "BaseDefense": 230, - "BaseStamina": 54, + "BaseDefense": 54, + "BaseStamina": 230, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -1622,6 +1625,9 @@ "Type I": [ "Normal" ], + "Type II": [ + "Fairy" + ], "Weaknesses": [ "Fighting" ], @@ -1643,8 +1649,8 @@ "Play Rough" ], "BaseAttack": 168, - "BaseDefense": 280, - "BaseStamina": 108, + "BaseDefense": 108, + "BaseStamina": 280, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -1687,8 +1693,8 @@ "Sludge Bomb" ], "BaseAttack": 88, - "BaseDefense": 80, - "BaseStamina": 90, + "BaseDefense": 90, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.2 }, @@ -1726,8 +1732,8 @@ "Poison Fang" ], "BaseAttack": 164, - "BaseDefense": 150, - "BaseStamina": 164, + "BaseDefense": 164, + "BaseStamina": 150, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -1774,8 +1780,8 @@ "Sludge Bomb" ], "BaseAttack": 134, - "BaseDefense": 90, - "BaseStamina": 130, + "BaseDefense": 130, + "BaseStamina": 90, "CaptureRate": 0.48, "FleeRate": 0.15 }, @@ -1824,8 +1830,8 @@ "Sludge Bomb" ], "BaseAttack": 162, - "BaseDefense": 120, - "BaseStamina": 158, + "BaseDefense": 158, + "BaseStamina": 120, "CaptureRate": 0.24, "FleeRate": 0.07 }, @@ -1867,8 +1873,8 @@ "Solar Beam" ], "BaseAttack": 202, - "BaseDefense": 150, - "BaseStamina": 190, + "BaseDefense": 190, + "BaseStamina": 150, "CaptureRate": 0.12, "FleeRate": 0.05 }, @@ -1913,8 +1919,8 @@ "X Scissor" ], "BaseAttack": 122, - "BaseDefense": 70, - "BaseStamina": 120, + "BaseDefense": 120, + "BaseStamina": 70, "CaptureRate": 0.32, "FleeRate": 0.15 }, @@ -1954,8 +1960,8 @@ "X Scissor" ], "BaseAttack": 162, - "BaseDefense": 120, - "BaseStamina": 170, + "BaseDefense": 170, + "BaseStamina": 120, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -1993,14 +1999,13 @@ } ], "Special Attack(s)": [ - "Dazzling Gleam", - "Psybeam", "Poison Fang", - "Shadow Ball" + "Psybeam", + "Signal Beam" ], "BaseAttack": 108, - "BaseDefense": 120, - "BaseStamina": 118, + "BaseDefense": 118, + "BaseStamina": 120, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -2038,8 +2043,8 @@ "Psychic" ], "BaseAttack": 172, - "BaseDefense": 140, - "BaseStamina": 154, + "BaseDefense": 154, + "BaseStamina": 140, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -2078,8 +2083,8 @@ "Rock Tomb" ], "BaseAttack": 108, - "BaseDefense": 20, - "BaseStamina": 86, + "BaseDefense": 86, + "BaseStamina": 20, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -2113,8 +2118,8 @@ "Stone Edge" ], "BaseAttack": 148, - "BaseDefense": 70, - "BaseStamina": 140, + "BaseDefense": 140, + "BaseStamina": 70, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -2151,8 +2156,8 @@ "Night Slash" ], "BaseAttack": 104, - "BaseDefense": 80, - "BaseStamina": 94, + "BaseDefense": 94, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -2184,8 +2189,8 @@ "Power Gem" ], "BaseAttack": 156, - "BaseDefense": 130, - "BaseStamina": 146, + "BaseDefense": 146, + "BaseStamina": 130, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -2223,8 +2228,8 @@ "Psybeam" ], "BaseAttack": 132, - "BaseDefense": 100, - "BaseStamina": 112, + "BaseDefense": 112, + "BaseStamina": 100, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -2257,8 +2262,8 @@ "Psychic" ], "BaseAttack": 194, - "BaseDefense": 160, - "BaseStamina": 176, + "BaseDefense": 176, + "BaseStamina": 160, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -2297,8 +2302,8 @@ "Low Sweep" ], "BaseAttack": 122, - "BaseDefense": 80, - "BaseStamina": 96, + "BaseDefense": 96, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -2332,8 +2337,8 @@ "Night Slash" ], "BaseAttack": 178, - "BaseDefense": 130, - "BaseStamina": 150, + "BaseDefense": 150, + "BaseStamina": 130, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -2450,8 +2455,8 @@ "Mud Bomb" ], "BaseAttack": 108, - "BaseDefense": 80, - "BaseStamina": 98, + "BaseDefense": 98, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -2495,8 +2500,8 @@ "Scald" ], "BaseAttack": 132, - "BaseDefense": 130, - "BaseStamina": 132, + "BaseDefense": 132, + "BaseStamina": 130, "CaptureRate": 0.2, "FleeRate": 0.07 }, @@ -2539,8 +2544,8 @@ "Submission" ], "BaseAttack": 180, - "BaseDefense": 180, - "BaseStamina": 202, + "BaseDefense": 202, + "BaseStamina": 180, "CaptureRate": 0.1, "FleeRate": 0.05 }, @@ -2582,8 +2587,8 @@ "Signal Beam" ], "BaseAttack": 110, - "BaseDefense": 50, - "BaseStamina": 76, + "BaseDefense": 76, + "BaseStamina": 50, "CaptureRate": 0.4, "FleeRate": 0.99 }, @@ -2628,8 +2633,8 @@ "Shadow Ball" ], "BaseAttack": 150, - "BaseDefense": 80, - "BaseStamina": 112, + "BaseDefense": 112, + "BaseStamina": 80, "CaptureRate": 0.2, "FleeRate": 0.07 }, @@ -2667,8 +2672,8 @@ "Shadow Ball" ], "BaseAttack": 186, - "BaseDefense": 110, - "BaseStamina": 152, + "BaseDefense": 152, + "BaseStamina": 110, "CaptureRate": 0.1, "FleeRate": 0.05 }, @@ -2711,8 +2716,8 @@ "Low Sweep" ], "BaseAttack": 118, - "BaseDefense": 140, - "BaseStamina": 96, + "BaseDefense": 96, + "BaseStamina": 140, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -2757,8 +2762,8 @@ "Submission" ], "BaseAttack": 154, - "BaseDefense": 160, - "BaseStamina": 144, + "BaseDefense": 144, + "BaseStamina": 160, "CaptureRate": 0.2, "FleeRate": 0.07 }, @@ -2844,8 +2849,8 @@ "Wrap" ], "BaseAttack": 158, - "BaseDefense": 100, - "BaseStamina": 78, + "BaseDefense": 78, + "BaseStamina": 100, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -2894,8 +2899,8 @@ "Sludge Bomb" ], "BaseAttack": 190, - "BaseDefense": 130, - "BaseStamina": 110, + "BaseDefense": 110, + "BaseStamina": 130, "CaptureRate": 0.2, "FleeRate": 0.07 }, @@ -2937,8 +2942,8 @@ "Solar Beam" ], "BaseAttack": 222, - "BaseDefense": 160, - "BaseStamina": 152, + "BaseDefense": 152, + "BaseStamina": 160, "CaptureRate": 0.1, "FleeRate": 0.05 }, @@ -2980,8 +2985,8 @@ "Wrap" ], "BaseAttack": 106, - "BaseDefense": 80, - "BaseStamina": 136, + "BaseDefense": 136, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -3018,8 +3023,8 @@ "Sludge Wave" ], "BaseAttack": 170, - "BaseDefense": 160, - "BaseStamina": 196, + "BaseDefense": 196, + "BaseStamina": 160, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -3068,8 +3073,8 @@ "Rock Tomb" ], "BaseAttack": 106, - "BaseDefense": 80, - "BaseStamina": 118, + "BaseDefense": 118, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -3120,8 +3125,8 @@ "Stone Edge" ], "BaseAttack": 142, - "BaseDefense": 110, - "BaseStamina": 156, + "BaseDefense": 156, + "BaseStamina": 110, "CaptureRate": 0.2, "FleeRate": 0.07 }, @@ -3165,8 +3170,8 @@ "Stone Edge" ], "BaseAttack": 176, - "BaseDefense": 160, - "BaseStamina": 198, + "BaseDefense": 198, + "BaseStamina": 160, "CaptureRate": 0.1, "FleeRate": 0.05 }, @@ -3205,8 +3210,8 @@ "Flame Wheel" ], "BaseAttack": 168, - "BaseDefense": 100, - "BaseStamina": 138, + "BaseDefense": 138, + "BaseStamina": 100, "CaptureRate": 0.32, "FleeRate": 0.1 }, @@ -3240,8 +3245,8 @@ "Heat Wave" ], "BaseAttack": 200, - "BaseDefense": 130, - "BaseStamina": 170, + "BaseDefense": 170, + "BaseStamina": 130, "CaptureRate": 0.12, "FleeRate": 0.06 }, @@ -3285,8 +3290,8 @@ "Water Pulse" ], "BaseAttack": 110, - "BaseDefense": 180, - "BaseStamina": 110, + "BaseDefense": 110, + "BaseStamina": 180, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -3325,8 +3330,8 @@ "Water Pulse" ], "BaseAttack": 184, - "BaseDefense": 190, - "BaseStamina": 198, + "BaseDefense": 198, + "BaseStamina": 190, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -3368,8 +3373,8 @@ "Thunderbolt" ], "BaseAttack": 128, - "BaseDefense": 50, - "BaseStamina": 138, + "BaseDefense": 138, + "BaseStamina": 50, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -3406,8 +3411,8 @@ "Magnet Bomb" ], "BaseAttack": 186, - "BaseDefense": 100, - "BaseStamina": 180, + "BaseDefense": 180, + "BaseStamina": 100, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -3437,8 +3442,8 @@ "Weight": "15.0 kg", "Height": "0.8 m", "BaseAttack": 138, - "BaseDefense": 104, - "BaseStamina": 132, + "BaseDefense": 132, + "BaseStamina": 104, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -3479,8 +3484,8 @@ "Swift" ], "BaseAttack": 126, - "BaseDefense": 70, - "BaseStamina": 96, + "BaseDefense": 96, + "BaseStamina": 70, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -3516,8 +3521,8 @@ "Drill Peck" ], "BaseAttack": 182, - "BaseDefense": 120, - "BaseStamina": 150, + "BaseDefense": 150, + "BaseStamina": 120, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -3555,8 +3560,8 @@ "Icy Wind" ], "BaseAttack": 104, - "BaseDefense": 130, - "BaseStamina": 138, + "BaseDefense": 138, + "BaseStamina": 130, "CaptureRate": 0.4, "FleeRate": 0.09 }, @@ -3594,8 +3599,8 @@ "Icy Wind" ], "BaseAttack": 156, - "BaseDefense": 180, - "BaseStamina": 192, + "BaseDefense": 192, + "BaseStamina": 180, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -3633,8 +3638,8 @@ "Sludge Bomb" ], "BaseAttack": 124, - "BaseDefense": 160, - "BaseStamina": 110, + "BaseDefense": 110, + "BaseStamina": 160, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -3667,8 +3672,8 @@ "Sludge Wave" ], "BaseAttack": 180, - "BaseDefense": 210, - "BaseStamina": 188, + "BaseDefense": 188, + "BaseStamina": 210, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -3706,8 +3711,8 @@ "Water Pulse" ], "BaseAttack": 120, - "BaseDefense": 60, - "BaseStamina": 112, + "BaseDefense": 112, + "BaseStamina": 60, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -3745,8 +3750,8 @@ "Icy Wind" ], "BaseAttack": 196, - "BaseDefense": 100, - "BaseStamina": 196, + "BaseDefense": 196, + "BaseStamina": 100, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -3793,8 +3798,8 @@ "Sludge Bomb" ], "BaseAttack": 136, - "BaseDefense": 60, - "BaseStamina": 82, + "BaseDefense": 82, + "BaseStamina": 60, "CaptureRate": 0.32, "FleeRate": 0.1 }, @@ -3843,8 +3848,8 @@ "Sludge Bomb" ], "BaseAttack": 172, - "BaseDefense": 90, - "BaseStamina": 118, + "BaseDefense": 118, + "BaseStamina": 90, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -3886,8 +3891,8 @@ "Sludge Wave" ], "BaseAttack": 204, - "BaseDefense": 120, - "BaseStamina": 156, + "BaseDefense": 156, + "BaseStamina": 120, "CaptureRate": 0.08, "FleeRate": 0.05 }, @@ -3921,8 +3926,8 @@ "Stone Edge" ], "BaseAttack": 90, - "BaseDefense": 70, - "BaseStamina": 186, + "BaseDefense": 186, + "BaseStamina": 70, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -3961,8 +3966,8 @@ "Psyshock" ], "BaseAttack": 104, - "BaseDefense": 120, - "BaseStamina": 140, + "BaseDefense": 140, + "BaseStamina": 120, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -3996,8 +4001,8 @@ "Shadow Ball" ], "BaseAttack": 162, - "BaseDefense": 170, - "BaseStamina": 196, + "BaseDefense": 196, + "BaseStamina": 170, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -4035,8 +4040,8 @@ "Water Pulse" ], "BaseAttack": 116, - "BaseDefense": 60, - "BaseStamina": 110, + "BaseDefense": 110, + "BaseStamina": 60, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -4069,8 +4074,8 @@ "X Scissor" ], "BaseAttack": 178, - "BaseDefense": 110, - "BaseStamina": 168, + "BaseDefense": 168, + "BaseStamina": 110, "CaptureRate": 0.16, "FleeRate": 0.07 }, @@ -4107,8 +4112,8 @@ "Thunderbolt" ], "BaseAttack": 102, - "BaseDefense": 80, - "BaseStamina": 124, + "BaseDefense": 124, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -4140,8 +4145,8 @@ "Thunderbolt" ], "BaseAttack": 150, - "BaseDefense": 120, - "BaseStamina": 174, + "BaseDefense": 174, + "BaseStamina": 120, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -4186,8 +4191,8 @@ "Seed Bomb" ], "BaseAttack": 110, - "BaseDefense": 120, - "BaseStamina": 132, + "BaseDefense": 132, + "BaseStamina": 120, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -4228,8 +4233,8 @@ "Solar Beam" ], "BaseAttack": 232, - "BaseDefense": 190, - "BaseStamina": 164, + "BaseDefense": 164, + "BaseStamina": 190, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -4268,8 +4273,8 @@ "Dig" ], "BaseAttack": 102, - "BaseDefense": 100, - "BaseStamina": 150, + "BaseDefense": 150, + "BaseStamina": 100, "CaptureRate": 0.32, "FleeRate": 0.1 }, @@ -4303,8 +4308,8 @@ "Earthquake" ], "BaseAttack": 140, - "BaseDefense": 120, - "BaseStamina": 202, + "BaseDefense": 202, + "BaseStamina": 120, "CaptureRate": 0.12, "FleeRate": 0.06 }, @@ -4326,20 +4331,14 @@ ], "Weight": "49.8 kg", "Height": "1.5 m", - "Next evolution(s)": [ - { - "Number": "107", - "Name": "Hitmonchan" - } - ], "Special Attack(s)": [ "Low Sweep", "Stomp", "Stone Edge" ], "BaseAttack": 148, - "BaseDefense": 100, - "BaseStamina": 172, + "BaseDefense": 172, + "BaseStamina": 100, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -4361,12 +4360,6 @@ ], "Weight": "50.2 kg", "Height": "1.4 m", - "Previous evolution(s)": [ - { - "Number": "106", - "Name": "Hitmonlee" - } - ], "Special Attack(s)": [ "Brick Break", "Fire Punch", @@ -4374,8 +4367,8 @@ "Thunder Punch" ], "BaseAttack": 138, - "BaseDefense": 100, - "BaseStamina": 204, + "BaseDefense": 204, + "BaseStamina": 100, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -4401,8 +4394,8 @@ "Stomp" ], "BaseAttack": 126, - "BaseDefense": 180, - "BaseStamina": 160, + "BaseDefense": 160, + "BaseStamina": 180, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -4440,8 +4433,8 @@ "Sludge Bomb" ], "BaseAttack": 136, - "BaseDefense": 80, - "BaseStamina": 142, + "BaseDefense": 142, + "BaseStamina": 80, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -4474,8 +4467,8 @@ "Sludge Bomb" ], "BaseAttack": 190, - "BaseDefense": 130, - "BaseStamina": 198, + "BaseDefense": 198, + "BaseStamina": 130, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -4520,8 +4513,8 @@ "Stomp" ], "BaseAttack": 110, - "BaseDefense": 160, - "BaseStamina": 116, + "BaseDefense": 116, + "BaseStamina": 160, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -4561,8 +4554,8 @@ "Stone Edge" ], "BaseAttack": 166, - "BaseDefense": 210, - "BaseStamina": 160, + "BaseDefense": 160, + "BaseStamina": 210, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -4588,8 +4581,8 @@ "Psychic" ], "BaseAttack": 40, - "BaseDefense": 500, - "BaseStamina": 60, + "BaseDefense": 60, + "BaseStamina": 500, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -4618,8 +4611,8 @@ "Solar Beam" ], "BaseAttack": 164, - "BaseDefense": 130, - "BaseStamina": 152, + "BaseDefense": 152, + "BaseStamina": 130, "CaptureRate": 0.32, "FleeRate": 0.09 }, @@ -4645,8 +4638,8 @@ "Stomp" ], "BaseAttack": 142, - "BaseDefense": 210, - "BaseStamina": 178, + "BaseDefense": 178, + "BaseStamina": 210, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -4684,8 +4677,8 @@ "Flash Cannon" ], "BaseAttack": 122, - "BaseDefense": 60, - "BaseStamina": 100, + "BaseDefense": 100, + "BaseStamina": 60, "CaptureRate": 0.4, "FleeRate": 0.1 }, @@ -4718,8 +4711,8 @@ "Hydro Pump" ], "BaseAttack": 176, - "BaseDefense": 110, - "BaseStamina": 150, + "BaseDefense": 150, + "BaseStamina": 110, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -4757,8 +4750,8 @@ "Water Pulse" ], "BaseAttack": 112, - "BaseDefense": 90, - "BaseStamina": 126, + "BaseDefense": 126, + "BaseStamina": 90, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -4830,8 +4823,8 @@ "Swift" ], "BaseAttack": 130, - "BaseDefense": 60, - "BaseStamina": 128, + "BaseDefense": 128, + "BaseStamina": 60, "CaptureRate": 0.4, "FleeRate": 0.15 }, @@ -4870,8 +4863,8 @@ "Psybeam" ], "BaseAttack": 194, - "BaseDefense": 120, - "BaseStamina": 192, + "BaseDefense": 192, + "BaseStamina": 120, "CaptureRate": 0.16, "FleeRate": 0.06 }, @@ -4882,6 +4875,9 @@ "Type I": [ "Psychic" ], + "Type II": [ + "Fairy" + ], "Weaknesses": [ "Bug", "Ghost", @@ -4899,8 +4895,8 @@ "Shadow Ball" ], "BaseAttack": 154, - "BaseDefense": 80, - "BaseStamina": 196, + "BaseDefense": 196, + "BaseStamina": 80, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -4933,8 +4929,8 @@ "X Scissor" ], "BaseAttack": 176, - "BaseDefense": 140, - "BaseStamina": 180, + "BaseDefense": 180, + "BaseStamina": 140, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -4968,8 +4964,8 @@ "Psyshock" ], "BaseAttack": 172, - "BaseDefense": 130, - "BaseStamina": 134, + "BaseDefense": 134, + "BaseStamina": 130, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -4995,8 +4991,8 @@ "Thunderbolt" ], "BaseAttack": 198, - "BaseDefense": 130, - "BaseStamina": 160, + "BaseDefense": 160, + "BaseStamina": 130, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -5024,8 +5020,8 @@ "Flamethrower" ], "BaseAttack": 214, - "BaseDefense": 130, - "BaseStamina": 158, + "BaseDefense": 158, + "BaseStamina": 130, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -5053,8 +5049,8 @@ "X Scissor" ], "BaseAttack": 184, - "BaseDefense": 130, - "BaseStamina": 186, + "BaseDefense": 186, + "BaseStamina": 130, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -5080,8 +5076,8 @@ "Iron Head" ], "BaseAttack": 148, - "BaseDefense": 150, - "BaseStamina": 184, + "BaseDefense": 184, + "BaseStamina": 150, "CaptureRate": 0.24, "FleeRate": 0.09 }, @@ -5116,8 +5112,8 @@ "Struggle" ], "BaseAttack": 42, - "BaseDefense": 40, - "BaseStamina": 84, + "BaseDefense": 84, + "BaseStamina": 40, "CaptureRate": 0.56, "FleeRate": 0.15 }, @@ -5153,8 +5149,8 @@ "Twister" ], "BaseAttack": 192, - "BaseDefense": 190, - "BaseStamina": 196, + "BaseDefense": 196, + "BaseStamina": 190, "CaptureRate": 0.08, "FleeRate": 0.07 }, @@ -5186,8 +5182,8 @@ "Ice Beam" ], "BaseAttack": 186, - "BaseDefense": 260, - "BaseStamina": 190, + "BaseDefense": 190, + "BaseStamina": 260, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -5210,8 +5206,8 @@ "Weight": "4.0 kg", "Height": "0.3 m", "BaseAttack": 110, - "BaseDefense": 96, - "BaseStamina": 110, + "BaseDefense": 110, + "BaseStamina": 96, "CaptureRate": 0.16, "FleeRate": 0.1 }, @@ -5256,8 +5252,8 @@ "Swift" ], "BaseAttack": 114, - "BaseDefense": 110, - "BaseStamina": 128, + "BaseDefense": 128, + "BaseStamina": 110, "CaptureRate": 0.32, "FleeRate": 0.1 }, @@ -5289,8 +5285,8 @@ "Water Pulse" ], "BaseAttack": 186, - "BaseDefense": 260, - "BaseStamina": 168, + "BaseDefense": 168, + "BaseStamina": 260, "CaptureRate": 0.12, "FleeRate": 0.06 }, @@ -5321,8 +5317,8 @@ "Thunderbolt" ], "BaseAttack": 192, - "BaseDefense": 130, - "BaseStamina": 174, + "BaseDefense": 174, + "BaseStamina": 130, "CaptureRate": 0.12, "FleeRate": 0.06 }, @@ -5355,8 +5351,8 @@ "Heat Wave" ], "BaseAttack": 238, - "BaseDefense": 130, - "BaseStamina": 178, + "BaseDefense": 178, + "BaseStamina": 130, "CaptureRate": 0.12, "FleeRate": 0.06 }, @@ -5382,8 +5378,8 @@ "Signal Beam" ], "BaseAttack": 156, - "BaseDefense": 130, - "BaseStamina": 158, + "BaseDefense": 158, + "BaseStamina": 130, "CaptureRate": 0.32, "FleeRate": 0.09 }, @@ -5426,8 +5422,8 @@ "Rock Tomb" ], "BaseAttack": 132, - "BaseDefense": 70, - "BaseStamina": 160, + "BaseDefense": 160, + "BaseStamina": 70, "CaptureRate": 0.32, "FleeRate": 0.09 }, @@ -5465,8 +5461,8 @@ "Rock Slide" ], "BaseAttack": 180, - "BaseDefense": 140, - "BaseStamina": 202, + "BaseDefense": 202, + "BaseStamina": 140, "CaptureRate": 0.12, "FleeRate": 0.05 }, @@ -5509,8 +5505,8 @@ "Rock Tomb" ], "BaseAttack": 148, - "BaseDefense": 60, - "BaseStamina": 142, + "BaseDefense": 142, + "BaseStamina": 60, "CaptureRate": 0.32, "FleeRate": 0.09 }, @@ -5548,8 +5544,8 @@ "Water Pulse" ], "BaseAttack": 190, - "BaseDefense": 120, - "BaseStamina": 190, + "BaseDefense": 190, + "BaseStamina": 120, "CaptureRate": 0.12, "FleeRate": 0.05 }, @@ -5582,8 +5578,8 @@ "Iron Head" ], "BaseAttack": 182, - "BaseDefense": 160, - "BaseStamina": 162, + "BaseDefense": 162, + "BaseStamina": 160, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -5609,8 +5605,8 @@ "Hyper Beam" ], "BaseAttack": 180, - "BaseDefense": 320, - "BaseStamina": 180, + "BaseDefense": 180, + "BaseStamina": 320, "CaptureRate": 0.16, "FleeRate": 0.09 }, @@ -5641,9 +5637,9 @@ "Weight": "55.4 kg", "Height": "1.7 m", "BaseAttack": 198, - "BaseDefense": 180, - "BaseStamina": 242, - "CaptureRate": 0.0, + "BaseDefense": 242, + "BaseStamina": 180, + "CaptureRate": 0, "FleeRate": 0.1 }, { @@ -5671,9 +5667,9 @@ "Weight": "52.6 kg", "Height": "1.6 m", "BaseAttack": 232, - "BaseDefense": 180, - "BaseStamina": 194, - "CaptureRate": 0.0, + "BaseDefense": 194, + "BaseStamina": 180, + "CaptureRate": 0, "FleeRate": 0.1 }, { @@ -5702,9 +5698,9 @@ "Weight": "60.0 kg", "Height": "2.0 m", "BaseAttack": 242, - "BaseDefense": 180, - "BaseStamina": 194, - "CaptureRate": 0.0, + "BaseDefense": 194, + "BaseStamina": 180, + "CaptureRate": 0, "FleeRate": 0.1 }, { @@ -5729,14 +5725,24 @@ "Family": 147, "Name": "Dratini candies" }, + "Next evolution(s)": [ + { + "Number": "148", + "Name": "Dragonair" + }, + { + "Number": "149", + "Name": "Dragonite" + } + ], "Special Attack(s)": [ "Aqua Tail", "Twister", "Wrap" ], "BaseAttack": 128, - "BaseDefense": 82, - "BaseStamina": 110, + "BaseDefense": 110, + "BaseStamina": 82, "CaptureRate": 0.32, "FleeRate": 0.09 }, @@ -5780,8 +5786,8 @@ "Wrap" ], "BaseAttack": 170, - "BaseDefense": 122, - "BaseStamina": 152, + "BaseDefense": 152, + "BaseStamina": 122, "CaptureRate": 0.08, "FleeRate": 0.06 }, @@ -5823,8 +5829,8 @@ "Hyper Beam" ], "BaseAttack": 250, - "BaseDefense": 182, - "BaseStamina": 212, + "BaseDefense": 212, + "BaseStamina": 182, "CaptureRate": 0.04, "FleeRate": 0.05 }, @@ -5852,9 +5858,9 @@ "Weight": "122.0 kg", "Height": "2.0 m", "BaseAttack": 284, - "BaseDefense": 212, - "BaseStamina": 202, - "CaptureRate": 0.0, + "BaseDefense": 202, + "BaseStamina": 212, + "CaptureRate": 0, "FleeRate": 0.1 }, { @@ -5886,9 +5892,9 @@ "Weight": "4.0 kg", "Height": "0.4 m", "BaseAttack": 220, - "BaseDefense": 200, - "BaseStamina": 220, - "CaptureRate": 0.0, + "BaseDefense": 220, + "BaseStamina": 200, + "CaptureRate": 0, "FleeRate": 0.1 } ] diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index ea81b7c093..d7f890933f 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1,26 +1,46 @@ import json +import logging import os + from pokemongo_bot.base_dir import _base_dir ''' Helper class for updating/retrieving Inventory data ''' -class _BaseInventoryComponent(object): - TYPE = None # base key name for items of this type - ID_FIELD = None # identifier field for items of this type + +# +# Abstraction + +class _StaticInventoryComponent(object): STATIC_DATA_FILE = None # optionally load static data from file, # dropping the data in a static variable named STATIC_DATA + STATIC_DATA = None def __init__(self): - self._data = {} if self.STATIC_DATA_FILE is not None: self.init_static_data() @classmethod def init_static_data(cls): if not hasattr(cls, 'STATIC_DATA') or cls.STATIC_DATA is None: - cls.STATIC_DATA = json.load(open(cls.STATIC_DATA_FILE)) + cls.STATIC_DATA = cls.process_static_data( + json.load(open(cls.STATIC_DATA_FILE))) + + @classmethod + def process_static_data(cls, data): + # optional hook for processing the static data + # default is to use the data directly + return data + + +class _BaseInventoryComponent(_StaticInventoryComponent): + TYPE = None # base key name for items of this type + ID_FIELD = None # identifier field for items of this type + + def __init__(self): + self._data = {} + super(_BaseInventoryComponent, self).__init__() def parse(self, item): # optional hook for parsing the dict for this item @@ -42,34 +62,22 @@ def retrieve_data(self, inventory): def refresh(self, inventory): self._data = self.retrieve_data(inventory) - def get(self, id): - return self._data.get(id) + def get(self, object_id): + return self._data.get(object_id) def all(self): return list(self._data.values()) -class Candy(object): - def __init__(self, family_id, quantity): - self.type = Pokemons.name_for(family_id) - self.quantity = quantity - - def consume(self, amount): - if self.quantity < amount: - raise Exception('Tried to consume more {} candy than you have'.format(self.type)) - self.quantity -= amount - - def add(self, amount): - if amount < 0: - raise Exception('Must add positive amount of candy') - self.quantity += amount +# +# Inventory Components class Candies(_BaseInventoryComponent): TYPE = 'candy' ID_FIELD = 'family_id' @classmethod - def family_id_for(self, pokemon_id): + def family_id_for(cls, pokemon_id): return Pokemons.first_evolution_id_for(pokemon_id) def get(self, pokemon_id): @@ -108,17 +116,120 @@ class Pokemons(_BaseInventoryComponent): ID_FIELD = 'id' STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'pokemon.json') - def parse(self, item): - if 'is_egg' in item: - return Egg(item) - return Pokemon(item) + @classmethod + def process_static_data(cls, data): + pokemon_id = 1 + for poke_info in data: + # prepare types + types = [poke_info['Type I'][0]] # required + for t in poke_info.get('Type II', []): + types.append(t) + poke_info['types'] = types + + # prepare attacks (moves) + cls._process_attacks(poke_info) + cls._process_attacks(poke_info, charged=True) + + # prepare movesets + poke_info['movesets'] = cls._process_movesets(poke_info, pokemon_id) + + # calculate maximum CP for the pokemon (best IVs, lvl 40) + base_attack = poke_info['BaseAttack'] + base_defense = poke_info['BaseDefense'] + base_stamina = poke_info['BaseStamina'] + max_cp = _calc_cp(base_attack, base_defense, base_stamina) + poke_info['max_cp'] = max_cp + + pokemon_id += 1 + return data + + @classmethod + def _process_movesets(cls, poke_info, pokemon_id): + # type: (dict, int) -> List[Moveset] + """ + The optimal moveset is the combination of two moves, one quick move + and one charge move, that deals the most damage over time. + + Because each quick move gains a certain amount of energy (different + for different moves) and each charge move requires a different amount + of energy to use, sometimes, a quick move with lower DPS will be + better since it charges the charge move faster. On the same note, + sometimes a charge move that has lower DPS will be more optimal since + it may require less energy or it may last for a longer period of time. + + Attacker have STAB (Same-type attack bonus - x1.25) pokemon have the + same type as attack. So we add it to the "Combo DPS" of the moveset. + + The defender attacks in intervals of 1 second for the first 2 attacks, + and then in intervals of 2 seconds for the remainder of the attacks. + This explains why we see two consecutive quick attacks at the beginning + of the match. As a result, we add +2 seconds to the DPS calculation + for defender DPS output. + + So to determine an optimal defensive moveset, we follow the same method + as we did for optimal offensive movesets, but instead calculate the + highest "Combo DPS" with an added 2 seconds to the quick move cool down. + + Note: critical hits have not yet been implemented in the game + + See http://pokemongo.gamepress.gg/optimal-moveset-explanation + See http://pokemongo.gamepress.gg/defensive-tactics + """ + + # Prepare movesets + movesets = [] + types = poke_info['types'] + for fm in poke_info['Fast Attack(s)']: + for chm in poke_info['Special Attack(s)']: + movesets.append(Moveset(fm, chm, types, pokemon_id)) + assert len(movesets) > 0 + + # Calculate attack perfection for each moveset + movesets = sorted(movesets, key=lambda m: m.dps_attack) + worst_dps = movesets[0].dps_attack + best_dps = movesets[-1].dps_attack + if best_dps > worst_dps: + for moveset in movesets: + current_dps = moveset.dps_attack + moveset.attack_perfection = \ + (current_dps - worst_dps) / (best_dps - worst_dps) + + # Calculate defense perfection for each moveset + movesets = sorted(movesets, key=lambda m: m.dps_defense) + worst_dps = movesets[0].dps_defense + best_dps = movesets[-1].dps_defense + if best_dps > worst_dps: + for moveset in movesets: + current_dps = moveset.dps_defense + moveset.defense_perfection = \ + (current_dps - worst_dps) / (best_dps - worst_dps) + + return sorted(movesets, key=lambda m: m.dps, reverse=True) + + @classmethod + def _process_attacks(cls, poke_info, charged=False): + # type: (dict, bool) -> List[Attack] + key = 'Fast Attack(s)' if not charged else 'Special Attack(s)' + moves_dict = (ChargedAttacks if charged else FastAttacks).BY_NAME + moves = [] + for name in poke_info[key]: + if name not in moves_dict: + raise KeyError('Unknown {} attack: "{}"'.format( + 'charged' if charged else 'fast', name)) + moves.append(moves_dict[name]) + moves = sorted(moves, key=lambda m: m.dps, reverse=True) + poke_info[key] = moves + assert len(moves) > 0 + return moves @classmethod def data_for(cls, pokemon_id): + # type: (int) -> dict return cls.STATIC_DATA[pokemon_id - 1] @classmethod def name_for(cls, pokemon_id): + # type: (int) -> string return cls.data_for(pokemon_id)['Name'] @classmethod @@ -129,24 +240,194 @@ def first_evolution_id_for(cls, pokemon_id): return pokemon_id @classmethod - def next_evolution_id_for(cls, pokemon_id): + def prev_evolution_id_for(cls, pokemon_id): + data = cls.data_for(pokemon_id) + if 'Previous evolution(s)' in data: + return int(data['Previous evolution(s)'][-1]['Number']) + return None + + @classmethod + def next_evolution_ids_for(cls, pokemon_id): try: - return int(cls.data_for(pokemon_id)['Next evolution(s)'][0]['Number']) + next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] except KeyError: - return None + return [] + # get only next level evolutions, not all possible + ids = [] + for p in next_evolutions: + p_id = int(p['Number']) + if cls.prev_evolution_id_for(p_id) == pokemon_id: + ids.append(p_id) + return ids @classmethod - def evolution_cost_for(cls, pokemon_id): + def last_evolution_ids_for(cls, pokemon_id): try: - return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] except KeyError: - return + return [pokemon_id] + # get only final evolutions, not all possible + ids = [] + for p in next_evolutions: + p_id = int(p['Number']) + if len(cls.data_for(p_id).get('Next evolution(s)', [])) == 0: + ids.append(p_id) + assert len(ids) > 0 + return ids + + @classmethod + def has_next_evolution(cls, pokemon_id): + poke_info = cls.data_for(pokemon_id) + return 'Next Evolution Requirements' in poke_info \ + or 'Next evolution(s)' in poke_info + + @classmethod + def evolution_cost_for(cls, pokemon_id): + if not cls.has_next_evolution(pokemon_id): + return None + return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + + def parse(self, item): + if 'is_egg' in item: + return Egg(item) + return Pokemon(item) def all(self): # by default don't include eggs in all pokemon (usually just # makes caller's lives more difficult) return [p for p in super(Pokemons, self).all() if not isinstance(p, Egg)] + +# +# Static Components + +class LevelToCPm(_StaticInventoryComponent): + """ + Data for the CP multipliers at different levels + See http://pokemongo.gamepress.gg/cp-multiplier + See https://github.com/justinleewells/pogo-optimizer/blob/edd692d/data/game/level-to-cpm.json + """ + + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'level_to_cpm.json') + MAX_LEVEL = 40 + MAX_CPM = .0 + # half of the lowest difference between CPMs + HALF_DIFF_BETWEEN_HALF_LVL = 14e-3 + + @classmethod + def init_static_data(cls): + super(LevelToCPm, cls).init_static_data() + cls.MAX_CPM = cls.cp_multiplier_for(cls.MAX_LEVEL) + + @classmethod + def cp_multiplier_for(cls, level): + # type: (Union[float, int, string]) -> float + level = float(level) + level = str(int(level) if level.is_integer() else level) + return cls.STATIC_DATA[level] + + @classmethod + def level_from_cpm(cls, cp_multiplier): + # type: (float) -> float + for lvl, cpm in cls.STATIC_DATA.iteritems(): + diff = abs(cpm - cp_multiplier) + if diff <= cls.HALF_DIFF_BETWEEN_HALF_LVL: + return float(lvl) + raise ValueError("Unknown cp_multiplier: {}".format(cp_multiplier)) + + +class _Attacks(_StaticInventoryComponent): + BY_NAME = {} # type: Dict[string, Attack] + BY_TYPE = {} # type: Dict[List[Attack]] + BY_DPS = [] # type: List[Attack] + + @classmethod + def process_static_data(cls, moves): + ret = {} + by_type = {} + by_name = {} + fast = cls is FastAttacks + for attack in moves: + attack = Attack(attack) if fast else ChargedAttack(attack) + ret[attack.id] = attack + by_name[attack.name] = attack + + if attack.type not in by_type: + by_type[attack.type] = [] + by_type[attack.type].append(attack) + + for t in by_type.iterkeys(): + attacks = sorted(by_type[t], key=lambda m: m.dps, reverse=True) + min_dps = attacks[-1].dps + max_dps = attacks[0].dps - min_dps + if max_dps > .0: + for attack in attacks: # type: Attack + attack.rate_in_type = (attack.dps - min_dps) / max_dps + by_type[t] = attacks + + cls.BY_NAME = by_name + cls.BY_TYPE = by_type + cls.BY_DPS = sorted(ret.values(), key=lambda m: m.dps, reverse=True) + + return ret + + @classmethod + def data_for(cls, attack_id): + # type: (int) -> Attack + if attack_id not in cls.STATIC_DATA: + raise ValueError("Attack {} not found in {}".format( + attack_id, cls.__name__)) + return cls.STATIC_DATA[attack_id] + + @classmethod + def by_name(cls, name): + # type: (string) -> Attack + return cls.BY_NAME[name] + + @classmethod + def list_for_type(cls, type_name): + # type: (string) -> List[Attack] + """ + :return: Attacks sorted by DPS in descending order + """ + return cls.BY_TYPE[type_name] + + @classmethod + def all(cls): + return cls.STATIC_DATA.values() + + @classmethod + def all_by_dps(cls): + return cls.BY_DPS + + +class FastAttacks(_Attacks): + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'fast_moves.json') + + +class ChargedAttacks(_Attacks): + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'charged_moves.json') + + +# +# Instances + +class Candy(object): + def __init__(self, family_id, quantity): + self.type = Pokemons.name_for(family_id) + self.quantity = quantity + + def consume(self, amount): + if self.quantity < amount: + raise Exception('Tried to consume more {} candy than you have'.format(self.type)) + self.quantity -= amount + + def add(self, amount): + if amount < 0: + raise Exception('Must add positive amount of candy') + self.quantity += amount + + class Egg(object): def __init__(self, data): self._data = data @@ -158,52 +439,306 @@ def has_next_evolution(self): class Pokemon(object): def __init__(self, data): self._data = data + # Unique ID for this particular Pokemon self.id = data['id'] + # Id of the such pokemons in pokedex self.pokemon_id = data['pokemon_id'] + + # Combat points value self.cp = data['cp'] + # Base CP multiplier, fixed at the catch time + self.cp_bm = data['cp_multiplier'] + # Changeable part of the CP multiplier, increasing at power up + self.cp_am = data.get('additional_cp_multiplier', .0) + # Resulting CP multiplier + self.cp_m = self.cp_bm + self.cp_am + + # Current pokemon level (half of level is a normal value) + self.level = LevelToCPm.level_from_cpm(self.cp_m) + + # Maximum health points + self.hp_max = data['stamina_max'] + # Current health points + self.hp = data.get('stamina', self.hp_max) + assert 0 <= self.hp <= self.hp_max + + # Individial Values of the current pokemon (different for each pokemon) + self.iv_attack = data.get('individual_attack', 0) + self.iv_defense = data.get('individual_defense', 0) + self.iv_stamina = data.get('individual_stamina', 0) + self._static_data = Pokemons.data_for(self.pokemon_id) self.name = Pokemons.name_for(self.pokemon_id) - self.iv = self._compute_iv() + self.nickname = data.get('nickname', self.name) + self.in_fort = 'deployed_fort_id' in data self.is_favorite = data.get('favorite', 0) is 1 + # Basic Values of the current pokemon (identical for all such pokemons) + self.base_attack = self._static_data['BaseAttack'] + self.base_defense = self._static_data['BaseDefense'] + self.base_stamina = self._static_data['BaseStamina'] + + # Maximum possible CP for the current pokemon + self.max_cp = self._static_data['max_cp'] + + self.fast_attack = FastAttacks.data_for(data['move_1']) + self.charged_attack = ChargedAttacks.data_for(data['move_2']) # type: ChargedAttack + + # Internal values (IV) perfection percent + self.iv = self._compute_iv_perfection() + + # IV CP perfection - kind of IV perfection percent but calculated + # using weight of each IV in its contribution to CP of the best + # evolution of current pokemon + # So it tends to be more accurate than simple IV perfection + self.ivcp = self._compute_cp_perfection() + + # Exact value of current CP (not rounded) + self.cp_exact = _calc_cp( + self.base_attack, self.base_defense, self.base_stamina, + self.iv_attack, self.iv_defense, self.iv_stamina, self.cp_m) + assert max(int(self.cp_exact), 10) == self.cp + + # Percent of maximum possible CP + self.cp_percent = self.cp_exact / self.max_cp + + # Get moveset instance with calculated DPS and perfection percents + self.moveset = self._get_moveset() + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + def can_evolve_now(self): - return self.has_next_evolution() and self.candy_quantity >= self.evolution_cost + return self.has_next_evolution() and \ + self.candy_quantity >= self.evolution_cost def has_next_evolution(self): - return 'Next Evolution Requirements' in self._static_data + return Pokemons.has_next_evolution(self.pokemon_id) def has_seen_next_evolution(self): - return pokedex().captured(self.next_evolution_id) + for pokemon_id in self.next_evolution_ids: + if pokedex().captured(pokemon_id): + return True + return False @property - def next_evolution_id(self): - return Pokemons.next_evolution_id_for(self.pokemon_id) + def family_id(self): + return self.first_evolution_id @property def first_evolution_id(self): return Pokemons.first_evolution_id_for(self.pokemon_id) + @property + def prev_evolution_id(self): + return Pokemons.prev_evolution_id_for(self.pokemon_id) + + @property + def next_evolution_ids(self): + return Pokemons.next_evolution_ids_for(self.pokemon_id) + + @property + def last_evolution_ids(self): + return Pokemons.last_evolution_ids_for(self.pokemon_id) + @property def candy_quantity(self): return candies().get(self.pokemon_id).quantity @property def evolution_cost(self): - return self._static_data['Next Evolution Requirements']['Amount'] + return Pokemons.evolution_cost_for(self.pokemon_id) + + def _compute_iv_perfection(self): + total_iv = self.iv_attack + self.iv_defense + self.iv_stamina + iv_perfection = round((total_iv / 45.0), 2) + return iv_perfection + + def _compute_cp_perfection(self): + """ + CP perfect percent is more accurate than IV perfect + + We know attack plays an important role in CP, and different + pokemons have different base value, that's means 15/14/15 is + better than 14/15/15 for lot of pokemons, and if one pokemon's + base def is more than base sta, 15/15/14 is better than 15/14/15. + + See https://github.com/jabbink/PokemonGoBot/issues/469 + + So calculate CP perfection at final level for the best of the final + evolutions of the pokemon. + """ + variants = [] + iv_attack = self.iv_attack + iv_defense = self.iv_defense + iv_stamina = self.iv_stamina + cp_m = LevelToCPm.MAX_CPM + last_evolution_ids = self.last_evolution_ids + for pokemon_id in last_evolution_ids: + poke_info = Pokemons.data_for(pokemon_id) + base_attack = poke_info['BaseAttack'] + base_defense = poke_info['BaseDefense'] + base_stamina = poke_info['BaseStamina'] + + # calculate CP variants at maximum level + worst_cp = _calc_cp(base_attack, base_defense, base_stamina, + 0, 0, 0, cp_m) + perfect_cp = _calc_cp(base_attack, base_defense, base_stamina, + cp_multiplier=cp_m) + current_cp = _calc_cp(base_attack, base_defense, base_stamina, + iv_attack, iv_defense, iv_stamina, cp_m) + cp_perfection = (current_cp - worst_cp) / (perfect_cp - worst_cp) + variants.append(cp_perfection) + + # get best value (probably for the best evolution) + cp_perfection = max(variants) + return cp_perfection + + def _get_moveset(self): + move1 = self.fast_attack + move2 = self.charged_attack + movesets = self._static_data['movesets'] + current_moveset = None + for moveset in movesets: # type: Moveset + if moveset.fast_attack == move1 and moveset.charged_attack == move2: + current_moveset = moveset + break + + if current_moveset is None: + error = "Unexpected moveset [{}, {}] for #{} {}," \ + " please update info in pokemon.json and create issue/PR"\ + .format(move1, move2, self.pokemon_id, self.name) + # raise ValueError(error) + logging.getLogger(type(self).__name__).error(error) + current_moveset = Moveset( + move1, move2, self._static_data['types'], self.pokemon_id) + + return current_moveset + + +class Attack(object): + def __init__(self, data): + # self._data = data # Not needed - all saved in fields + self.id = data['id'] + self.name = data['name'] + self.type = data['type'] + self.damage = data['damage'] + self.duration = data['duration'] / 1000.0 # duration in seconds - def _compute_iv(self): - total_IV = 0.0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] + # Energy addition for fast attack + # Energy cost for charged attack + self.energy = data['energy'] - for individual_stat in iv_stats: - try: - total_IV += self._data[individual_stat] - except Exception: - self._data[individual_stat] = 0 - continue - pokemon_potential = round((total_IV / 45.0), 2) - return pokemon_potential + # Damage Per Second + # recalc for better precision + self.dps = self.damage / self.duration + + # Perfection of the attack in it's type (from 0 to 1) + self.rate_in_type = .0 + + @property + def damage_with_stab(self): + # damage with STAB (Same-type attack bonus) + return self.damage * STAB_FACTOR + + @property + def dps_with_stab(self): + # DPS with STAB (Same-type attack bonus) + return self.dps * STAB_FACTOR + + @property + def energy_per_second(self): + return self.energy / self.duration + + @property + def dodge_window(self): + # TODO: Attack Dodge Window + return NotImplemented + + @property + def is_charged(self): + return False + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + + +class ChargedAttack(Attack): + def __init__(self, data): + super(ChargedAttack, self).__init__(data) + + @property + def is_charged(self): + return True + + +class Moveset(object): + def __init__(self, fm, chm, pokemon_types=(), pokemon_id=-1): + # type: (Attack, ChargedAttack, List[string], int) -> None + if len(pokemon_types) <= 0 < pokemon_id: + pokemon_types = Pokemons.data_for(pokemon_id)['types'] + + self.pokemon_id = pokemon_id + self.fast_attack = fm + self.charged_attack = chm + + # See Pokemons._process_movesets() + # See http://pokemongo.gamepress.gg/optimal-moveset-explanation + # See http://pokemongo.gamepress.gg/defensive-tactics + + fm_number = 100 # for simplicity we use 100 + + fm_energy = fm.energy * fm_number + fm_damage = fm.damage * fm_number + fm_secs = fm.duration * fm_number + + # Defender attacks in intervals of 1 second for the + # first 2 attacks, and then in intervals of 2 seconds + # So add 1.95 seconds to the quick move cool down for defense + # 1.95 is something like an average here + # TODO: Do something better? + fm_defense_secs = (fm.duration + 1.95) * fm_number + + chm_number = fm_energy / chm.energy + chm_damage = chm.damage * chm_number + chm_secs = chm.duration * chm_number + + damage_sum = fm_damage + chm_damage + # raw Damage-Per-Second for the moveset + self.dps = damage_sum / (fm_secs + chm_secs) + # average DPS for defense + self.dps_defense = damage_sum / (fm_defense_secs + chm_secs) + + # apply STAB (Same-type attack bonus) + if fm.type in pokemon_types: + fm_damage *= STAB_FACTOR + if chm.type in pokemon_types: + chm_damage *= STAB_FACTOR + + # DPS for attack (counting STAB) + self.dps_attack = (fm_damage + chm_damage) / (fm_secs + chm_secs) + + # Moveset perfection percent attack and for defense + # Calculated for current pokemon, not between all pokemons + # So 100% perfect moveset can be weak if pokemon is weak (e.g. Caterpie) + self.attack_perfection = .0 + self.defense_perfection = .0 + + # TODO: True DPS for real combat (floor(Attack/200 * MovePower * STAB) + 1) + # See http://pokemongo.gamepress.gg/pokemon-attack-explanation + + def __str__(self): + return '[{}, {}]'.format(self.fast_attack, self.charged_attack) + + def __repr__(self): + return '[{}, {}]'.format(self.fast_attack, self.charged_attack) class Inventory(object): @@ -227,8 +762,47 @@ def refresh(self): with open(user_web_inventory, 'w') as outfile: json.dump(inventory, outfile) +# +# Usage helpers + +# STAB (Same-type attack bonus) +STAB_FACTOR = 1.25 _inventory = None +LevelToCPm() # init LevelToCPm +FastAttacks() # init FastAttacks +ChargedAttacks() # init ChargedAttacks + + +def _calc_cp(base_attack, base_defense, base_stamina, + iv_attack=15, iv_defense=15, iv_stamina=15, + cp_multiplier=LevelToCPm.MAX_CPM): + """ + CP calculation + + CP = (Attack * Defense^0.5 * Stamina^0.5 * CP_Multiplier^2) / 10 + CP = (BaseAtk+AtkIV) * (BaseDef+DefIV)^0.5 * (BaseStam+StamIV)^0.5 * Lvl(CPScalar)^2 / 10 + + See https://www.reddit.com/r/TheSilphRoad/comments/4t7r4d/exact_pokemon_cp_formula/ + See https://www.reddit.com/r/pokemongodev/comments/4t7xb4/exact_cp_formula_from_stats_and_cpm_and_an_update/ + See http://pokemongo.gamepress.gg/pokemon-stats-advanced + See http://pokemongo.gamepress.gg/cp-multiplier + See http://gaming.stackexchange.com/questions/280491/formula-to-calculate-pokemon-go-cp-and-hp + + :param base_attack: Pokemon BaseAttack + :param base_defense: Pokemon BaseDefense + :param base_stamina: Pokemon BaseStamina + :param iv_attack: Pokemon IndividualAttack (0..15) + :param iv_defense: Pokemon IndividualDefense (0..15) + :param iv_stamina: Pokemon IndividualStamina (0..15) + :param cp_multiplier: CP Multiplier (0.79030001 is max - value for level 40) + :return: CP as float + """ + return (base_attack + iv_attack) \ + * ((base_defense + iv_defense)**0.5) \ + * ((base_stamina + iv_stamina)**0.5) \ + * (cp_multiplier ** 2) / 10 + def init_inventory(bot): global _inventory @@ -257,3 +831,15 @@ def pokemons(refresh=False): def items(): return _inventory.items + + +def levels_to_cpm(): + return LevelToCPm + + +def fast_attacks(): + return FastAttacks + + +def charged_attacks(): + return ChargedAttacks diff --git a/tests/inventory_test.py b/tests/inventory_test.py new file mode 100644 index 0000000000..3d5ffd66b6 --- /dev/null +++ b/tests/inventory_test.py @@ -0,0 +1,183 @@ +import unittest + +from pokemongo_bot.inventory import * + + +class InventoryTest(unittest.TestCase): + def test_pokemons(self): + # Init data + self.assertEqual(len(Pokemons().all()), 0) # No inventory loaded here + + obj = Pokemons + self.assertEqual(len(obj.STATIC_DATA), 151) + + for poke_info in obj.STATIC_DATA: + name = poke_info['Name'] + pokemon_id = int(poke_info['Number']) + self.assertTrue(1 <= pokemon_id <= 151) + + self.assertGreaterEqual(len(poke_info['movesets']), 1) + self.assertTrue(262 <= poke_info['max_cp'] <= 4145) + self.assertTrue(1 <= len(poke_info['types']) <= 2) + self.assertTrue(40 <= poke_info['BaseAttack'] <= 284) + self.assertTrue(54 <= poke_info['BaseDefense'] <= 242) + self.assertTrue(20 <= poke_info['BaseStamina'] <= 500) + self.assertTrue(.0 <= poke_info['CaptureRate'] <= .56) + self.assertTrue(.0 <= poke_info['FleeRate'] <= .99) + self.assertTrue(1 <= len(poke_info['Weaknesses']) <= 7) + self.assertTrue(3 <= len(name) <= 10) + + self.assertGreaterEqual(len(poke_info['Classification']), 11) + self.assertGreaterEqual(len(poke_info['Fast Attack(s)']), 1) + self.assertGreaterEqual(len(poke_info['Special Attack(s)']), 1) + + self.assertIs(obj.data_for(pokemon_id), poke_info) + self.assertIs(obj.name_for(pokemon_id), name) + + first_evolution_id = obj.first_evolution_id_for(pokemon_id) + self.assertGreaterEqual(first_evolution_id, 1) + next_evolution_ids = obj.next_evolution_ids_for(pokemon_id) + last_evolution_ids = obj.last_evolution_ids_for(pokemon_id) + candies_cost = obj.evolution_cost_for(pokemon_id) + obj.prev_evolution_id_for(pokemon_id) # just call test + self.assertGreaterEqual(len(last_evolution_ids), 1) + + if not obj.has_next_evolution(pokemon_id): + assert 'Next evolution(s)' not in poke_info + assert 'Next Evolution Requirements' not in poke_info + else: + self.assertGreaterEqual(len(next_evolution_ids), 1) + self.assertLessEqual(len(next_evolution_ids), len(last_evolution_ids)) + + reqs = poke_info['Next Evolution Requirements'] + self.assertEqual(reqs["Family"], first_evolution_id) + candies_name = obj.name_for(first_evolution_id) + ' candies' + self.assertEqual(reqs["Name"], candies_name) + self.assertIsNotNone(candies_cost) + self.assertTrue(12 <= candies_cost <= 400) + self.assertEqual(reqs["Amount"], candies_cost) + + evolutions = poke_info["Next evolution(s)"] + self.assertGreaterEqual(len(evolutions), len(next_evolution_ids)) + + for p in evolutions: + p_id = int(p["Number"]) + self.assertNotEqual(p_id, pokemon_id) + self.assertEqual(p["Name"], obj.name_for(p_id)) + + for p_id in next_evolution_ids: + self.assertEqual(obj.prev_evolution_id_for(p_id), pokemon_id) + prev_evs = obj.data_for(p_id)["Previous evolution(s)"] + self.assertGreaterEqual(len(prev_evs), 1) + self.assertEqual(int(prev_evs[-1]["Number"]), pokemon_id) + self.assertEqual(prev_evs[-1]["Name"], name) + + # Only Eevee has 3 next evolutions + self.assertEqual(len(next_evolution_ids), + 1 if pokemon_id != 133 else 3) + + if "Previous evolution(s)" in poke_info: + for p in poke_info["Previous evolution(s)"]: + p_id = int(p["Number"]) + self.assertNotEqual(p_id, pokemon_id) + self.assertEqual(p["Name"], obj.name_for(p_id)) + + # + # Specific pokemons testing + + poke = Pokemon({ + "num_upgrades": 2, "move_1": 210, "move_2": 69, "pokeball": 2, + "favorite": 1, "pokemon_id": 42, "battles_attacked": 4, + "stamina": 76, "stamina_max": 76, "individual_attack": 9, + "individual_defense": 4, "individual_stamina": 8, + "cp_multiplier": 0.4627983868122101, + "additional_cp_multiplier": 0.018886566162109375, + "cp": 653, "nickname": "Golb", "id": 13632861873471324}) + self.assertEqual(poke.level, 12.5) + self.assertEqual(poke.iv, 0.47) + self.assertAlmostEqual(poke.ivcp, 0.488747515) + self.assertAlmostEqual(poke.max_cp, 1921.34561459) + self.assertAlmostEqual(poke.cp_percent, 0.340368964) + self.assertTrue(poke.is_favorite) + self.assertEqual(poke.name, 'Golbat') + self.assertEqual(poke.nickname, "Golb") + self.assertAlmostEqual(poke.moveset.dps, 10.7540173053) + self.assertAlmostEqual(poke.moveset.dps_attack, 12.14462299) + self.assertAlmostEqual(poke.moveset.dps_defense, 4.876681614) + self.assertAlmostEqual(poke.moveset.attack_perfection, 0.4720730048) + self.assertAlmostEqual(poke.moveset.defense_perfection, 0.8158081497) + + poke = Pokemon({ + "move_1": 221, "move_2": 129, "pokemon_id": 19, "cp": 106, + "individual_attack": 6, "stamina_max": 22, "individual_defense": 14, + "cp_multiplier": 0.37523558735847473, "id": 7841053399}) + self.assertEqual(poke.level, 7.5) + self.assertEqual(poke.iv, 0.44) + self.assertAlmostEqual(poke.ivcp, 0.3804059) + self.assertAlmostEqual(poke.max_cp, 581.64643575) + self.assertAlmostEqual(poke.cp_percent, 0.183759867) + self.assertFalse(poke.is_favorite) + self.assertEqual(poke.name, 'Rattata') + self.assertEqual(poke.nickname, 'Rattata') + self.assertAlmostEqual(poke.moveset.dps, 12.5567813108) + self.assertAlmostEqual(poke.moveset.dps_attack, 15.6959766385) + self.assertAlmostEqual(poke.moveset.dps_defense, 5.54282440561) + self.assertAlmostEqual(poke.moveset.attack_perfection, 0.835172881385) + self.assertAlmostEqual(poke.moveset.defense_perfection, 0.603137650999) + + def test_levels_to_cpm(self): + l2c = LevelToCPm + self.assertIs(levels_to_cpm(), l2c) + max_cpm = l2c.cp_multiplier_for(l2c.MAX_LEVEL) + self.assertEqual(l2c.MAX_LEVEL, 40) + self.assertEqual(l2c.MAX_CPM, max_cpm) + self.assertEqual(len(l2c.STATIC_DATA), 79) + + self.assertEqual(l2c.cp_multiplier_for("1"), 0.094) + self.assertEqual(l2c.cp_multiplier_for(1), 0.094) + self.assertEqual(l2c.cp_multiplier_for(1.0), 0.094) + self.assertEqual(l2c.cp_multiplier_for("17.5"), 0.558830576) + self.assertEqual(l2c.cp_multiplier_for(17.5), 0.558830576) + self.assertEqual(l2c.cp_multiplier_for('40.0'), 0.79030001) + self.assertEqual(l2c.cp_multiplier_for(40.0), 0.79030001) + self.assertEqual(l2c.cp_multiplier_for(40), 0.79030001) + + self.assertEqual(l2c.level_from_cpm(0.79030001), 40.0) + self.assertEqual(l2c.level_from_cpm(0.7903), 40.0) + + def test_attacks(self): + self._test_attacks(fast_attacks, FastAttacks) + self._test_attacks(charged_attacks, ChargedAttacks) + + def _test_attacks(self, callback, clazz): + charged = clazz is ChargedAttacks + self.assertIs(callback(), clazz) + + # check consistency + attacks = clazz.all_by_dps() + number = len(attacks) + self.assertTrue(number > 0) + self.assertGreaterEqual(len(clazz.BY_TYPE), 17) + self.assertEqual(number, len(clazz.all())) + self.assertEqual(number, len(clazz.STATIC_DATA)) + self.assertEqual(number, len(clazz.BY_NAME)) + self.assertEqual(number, sum([len(l) for l in clazz.BY_TYPE.values()])) + + # check data + prev_dps = float("inf") + for attack in attacks: # type: Attack + self.assertGreater(attack.id, 0) + self.assertGreater(len(attack.name), 0) + self.assertGreater(len(attack.type), 0) + self.assertGreaterEqual(attack.damage, 0) + self.assertGreater(attack.duration, .0) + self.assertGreater(attack.energy, 0) + self.assertGreaterEqual(attack.dps, 0) + self.assertTrue(.0 <= attack.rate_in_type <= 1.0) + self.assertLessEqual(attack.dps, prev_dps) + self.assertEqual(attack.is_charged, charged) + self.assertIs(attack, clazz.data_for(attack.id)) + self.assertIs(attack, clazz.by_name(attack.name)) + self.assertTrue(attack in clazz.BY_TYPE[attack.type]) + self.assertIsInstance(attack, ChargedAttack if charged else Attack) + prev_dps = attack.dps From f7975bb33cbc5e9a735782a0bc9dcf961a75b6ec Mon Sep 17 00:00:00 2001 From: Eli White Date: Wed, 10 Aug 2016 21:45:02 -0700 Subject: [PATCH 096/143] Blacklisting tejado from getting mentioned by the mention-bot --- .mention-bot | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .mention-bot diff --git a/.mention-bot b/.mention-bot new file mode 100644 index 0000000000..5635897ab2 --- /dev/null +++ b/.mention-bot @@ -0,0 +1,3 @@ +{ + "userBlacklist": ["tejado"] +} From a8242f7bda8cfb1b726173018a903fd4c58f87cc Mon Sep 17 00:00:00 2001 From: Eli White Date: Wed, 10 Aug 2016 22:00:09 -0700 Subject: [PATCH 097/143] Moving wiki pages to docs folder --- docs/auto_restart.md | 73 ++++++++++ docs/configuration_files.md | 271 ++++++++++++++++++++++++++++++++++++ docs/develop.md | 28 ++++ docs/docker.md | 25 ++++ docs/faq.md | 49 +++++++ docs/google_map.md | 54 +++++++ docs/installation.md | 93 +++++++++++++ docs/pokemon_iv.md | 58 ++++++++ 8 files changed, 651 insertions(+) create mode 100644 docs/auto_restart.md create mode 100644 docs/configuration_files.md create mode 100644 docs/develop.md create mode 100644 docs/docker.md create mode 100644 docs/faq.md create mode 100644 docs/google_map.md create mode 100644 docs/installation.md create mode 100644 docs/pokemon_iv.md diff --git a/docs/auto_restart.md b/docs/auto_restart.md new file mode 100644 index 0000000000..e85752018c --- /dev/null +++ b/docs/auto_restart.md @@ -0,0 +1,73 @@ +This page is for a workaround to restart your bot(s). +_(Restarting is superior over reconnecting in case of stability for crashes)_ + +# MAC OS +1. Open your terminal +Just open it and you finished step 1 + +2. Create a new apple script +Heres an example to start and restart bots (in separate folders) adjust it for your needs. (paths, start commands, restart timer, ...) + +You can create a start file (if you are lazy :P) and a restart file or just one for both needs + +Start script: + + tell application "Terminal" + activate + do script "cd desktop" in selected tab of the front window #edit your path + do script "cd bots" in selected tab of the front window #edit your path + do script "cd bot1" in selected tab of the front window #edit your path + do script "python pokecli.py" in selected tab of the front window #start with your parameters + + #add more bots + delay 10 + tell application "System Events" + keystroke "t" using {command down} #open a new tab for next bot + end tell + delay 5 + do script "cd .." in selected tab of the front window + do script "cd bot2" in selected tab of the front window + do script "python pokecli.py" in selected tab of the front window + #copy this part for the amount you need + end tell + +restart: + + repeat + + delay 1200 #timer in seconds + tell application "Terminal" + activate + + tell application "System Events" + keystroke "c" using {control down} #close the bot + end tell + delay 3 + do script "clear" in selected tab of the front window #not needed just for nice view + delay 3 + do script "python pokecli.py" in selected tab of the front window #restart with parameters + + #copy for the amount of bots + delay 10 + tell application "System Events" + keystroke "ö" using {command down} #going to the previous tab + end tell + delay 3 + + tell application "System Events" + keystroke "c" using {control down} + end tell + delay 3 + do script "clear" in selected tab of the front window + delay 3 + do script "python pokecli.py" in selected tab of the front window + + #copy for the amount of bots + tell application "System Events" + keystroke "ä" using {command down} #moving the the last tab + end tell + delay 3 + + end tell + + end repeat diff --git a/docs/configuration_files.md b/docs/configuration_files.md new file mode 100644 index 0000000000..a881740b9a --- /dev/null +++ b/docs/configuration_files.md @@ -0,0 +1,271 @@ +## Usage (up-to-date) + 1. copy `config.json.example` to `config.json`. + 2. Edit `config.json` and replace `auth_service`, `username`, `password`, `location` and `gmapkey` with your parameters (other keys are optional, check `Advance Configuration` below) + 3. Simply launch the script with : `./run.sh` or `./pokecli.py` or `python pokecli.py -cf ./configs/config.json` if you want to specify a config file + +## Advanced Configuration +| Parameter | Default | Description | +|------------------|-------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `tasks` | [] | The behaviors you want the bot to do. Read [how to configure tasks](#configuring-tasks). +| `max_steps` | 5 | The steps around your initial location (DEFAULT 5 mean 25 cells around your location) that will be explored +| `forts.avoid_circles` | False | Set whether the bot should avoid circles | +| `forts.max_circle_size` | 10 | How many forts to keep in ignore list | +| `walk` | 4.16 | Set the walking speed in kilometers per hour. (14 km/h is the maximum speed for egg hatching) | +| `action_wait_min` | 1 | Set the minimum time setting for anti-ban time randomizer +| `action_wait_max` | 4 | Set the maximum time setting for anti-ban time randomizer +| `debug` | false | Let the default value here except if you are developer | +| `test` | false | Let the default value here except if you are developer | | +| `location_cache` | true | Bot will start at last known location if you do not have location set in the config | +| `distance_unit` | km | Set the unit to display distance in (km for kilometers, mi for miles, ft for feet) | +| `evolve_cp_min` | 300 | Min. CP for evolve_all function + +## Configuring Tasks +The behaviors of the bot are configured via the `tasks` key in the `config.json`. This enables you to list what you want the bot to do and change the priority of those tasks by reordering them in the list. This list of tasks is run repeatedly and in order. For more information on why we are moving config to this format, check out the [original proposal](https://github.com/PokemonGoF/PokemonGo-Bot/issues/142). + +### Task Options: +* CatchLuredPokemon +* CatchVisiblePokemon +* EvolvePokemon + * `evolve_all`: Default `NONE` | Set to `"all"` to evolve Pokémon if possible when the bot starts. Can also be set to individual Pokémon as well as multiple separated by a comma. e.g "Pidgey,Rattata,Weedle,Zubat" + * `evolve_speed`: Default `20` + * `use_lucky_egg`: Default: `False` +* FollowPath + * `path_mode`: Default `loop` | Set the mode for the path navigator (loop or linear). + * `path_file`: Default `NONE` | Set the file containing the waypoints for the path navigator. +* FollowSpiral +* HandleSoftBan +* IncubateEggs + * `longer_eggs_first`: Default `True` +* MoveToFort +* [MoveToMapPokemon](#sniping-movetolocation) +* NicknamePokemon + * `nickname_template`: Default `""` | See the [Pokemon Nicknaming](#pokemon-nicknaming) section for more details +* RecycleItems + * `item_filter`: Pass a list of unwanted [items (using their JSON codes)](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Item-ID's) to recycle when collected at a Pokestop +* SpinFort +* TransferPokemon + +### Example configuration: +The following configuration tells the bot to transfer all the Pokemon that match the transfer configuration rules, then recycle the items that match its configuration, then catch the pokemon that it can, so on, so forth. Note the last two tasks, MoveToFort and FollowSpiral. When a task is still in progress, it won't run the next things in the list. So it will move towards the fort, on each step running through the list of tasks again. Only when it arrives at the fort and there are no other stops available for it to move towards will it continue to the next step and follow the spiral. + +``` +{ + // ... + "tasks": [ + { + "type": "TransferPokemon" + }, + { + "type": "RecycleItems" + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort" + }, + { + "type": "MoveToFort" + }, + { + "type": "FollowSpiral" + } + ] + // ... +} +``` + +### Specifying configuration for tasks +If you want to configure a given task, you can pass values like this: + +``` +{ + // ... + "tasks": [ + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + } + ] + // ... +} +``` + +### An example task configuration if you only wanted to collect items from forts: +``` +{ + // ... + "tasks": [ + { + "type": "RecycleItems" + }, + { + "type": "SpinFortWorker" + }, + { + "type": "MoveToFortWorker" + } + ], + // ... +} +``` + +## Catch Configuration +Default configuration will capture all Pokémon. + +```"any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}``` + +You can override the global configuration with Pokémon-specific options, such as: + +```"Pidgey": {"catch_above_cp": 0, "catch_above_iv": 0.8", "logic": "and"}``` to only capture Pidgey with a good roll. + +Additionally, you can specify always_capture and never_capture flags. + +For example: ```"Pidgey": {"never_capture": true}``` will stop catching Pidgey entirely. + +## Release Configuration + +### Common configuration + +Default configuration will not release any Pokémon. + +```"release": {"any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}}``` + +You can override the global configuration with Pokémon-specific options, such as: + +```"release": {"Pidgey": {"release_below_cp": 0, "release_below_iv": 0.8, "logic": "or"}}``` to only release Pidgey with bad rolls. + +Additionally, you can specify always_release and never_release flags. For example: + +```"release": {"Pidgey": {"always_release": true}}``` will release all Pidgey caught. + +### Keep the strongest pokemon configuration (dev branch) + +You can set ```"release": {"Pidgey": {"keep_best_cp": 1}}``` or ```"release": {"any": {"keep_best_iv": 1}}```. + +In that case after each capture bot will check that do you have a new Pokémon or not. + +If you don't have it, it will keep it (no matter was it strong or weak Pokémon). + +If you already have it, it will keep a stronger version and will transfer the a weaker one. + +```"release": {"any": {"keep_best_cp": 2}}```, ```"release": {"any": {"keep_best_cp": 10}}``` - can be any number. + +## Evolve All Configuration + +By setting the `evolve_all` attribute in config.json, you can instruct the bot to automatically +evolve specified Pokémon on startup. This is especially useful for batch-evolving after popping up +a lucky egg (currently this needs to be done manually). + +The evolve all mechanism evolves only higher IV/CP Pokémon. It works by sorting the high CP Pokémon (default: 300 CP or higher) +based on their IV values. After evolving all high CP Pokémon, the mechanism will move on to evolving lower CP Pokémon +only based on their CP (if it can). +It will also automatically transfer the evolved Pokémon based on the release configuration. + +Examples on how to use (set in config.json): + +1. "evolve_all": "all" + Will evolve ALL Pokémon. + +2. "evolve_all": "Pidgey,Weedle" + Will only evolve Pidgey and Weedle. + +3. Not setting evolve_all or having any other string would not evolve any Pokémon on startup. + +If you wish to change the default threshold of 300 CP, simply add the following to the config file: + +``` +"evolve_cp_min": +``` + +## Path Navigator Configuration + +Setting the `navigator.type` setting to `path` allows you to specify waypoints which the bot will follow. The waypoints can be loaded from a GPX or JSON file. By default the bot will walk along all specified waypoints and then move directly to the first waypoint again. When setting `navigator.path_mode` to `linear`, the bot will turn around at the last waypoint and along the given waypoints in reverse order. + +An example for a JSON file can be found in `configs/path.example.json`. GPX files can be exported from many online tools, such as gpsies.com.The bot loads the first segment of the first track. + +## Pokemon Nicknaming + +A `nickname_template` can be specified for the `NicknamePokemon` task to allow a nickname template to be applied to all pokemon in the user's inventory. For example, a user wanting all their pokemon to have their IV values as their nickname could use a template `{iv_ads}`, which will cause their pokemon to be named something like `13/7/12` (depending on the pokemon's actual IVs). + +The `NicknamePokemon` task will rename all pokemon in inventory on startup to match the given template and will rename any newly caught/hatched/evolved pokemon as the bot runs. _It may take one or two "ticks" after catching/hatching/evolving a pokemon for it to be renamed. This is intended behavior._ + +> **NOTE:** If you experience frequent `Pokemon not found` error messages, this is because the inventory cache has not been updated after a pokemon was released. This can be remedied by placing the `NicknamePokemon` task above the `TransferPokemon` task in your `config.json` file. + +Niantic imposes a 12-character limit on all pokemon nicknames, so any new nickname will be truncated to 12 characters if over that limit. Thus, it is up to the user to exercise judgment on what template will best suit their need with this constraint in mind. + +Because some pokemon have very long names, you can use the [Format String syntax](https://docs.python.org/2.7/library/string.html#formatstrings) to ensure that your names do not cause your templates to truncate. For example, using `{name:.8s}` causes the Pokemon name to never take up more than 8 characters in the nickname. This would help guarantee that a template like `{name:.8s}_{iv_pct}` never goes over the 12-character limit. + +Valid names in templates are: +- `name` = pokemon name +- `id` = pokemon type id (e.g. 1 for Bulbasaurs) +- `cp` = pokemon's CP +- `iv_attack` = pokemon's attack IV +- `iv_defense` = pokemon's defense IV +- `iv_stamina` = pokemon's stamina IV +- `iv_ads` = pokemon's IVs in `(attack)/(defense)/(stamina)` format (matches web UI format -- A/D/S) +- `iv_sum` = pokemon's IVs as a sum (e.g. 45 when 3 perfect 15 IVs) +- `iv_pct` = pokemon's IVs as a percentage (0-100) + +> **NOTE:** Use a blank template (`""`) to revert all pokemon to their original names (as if they had no nickname). + +Sample usages: +- `"{name}_{iv_pct}"` => `Mankey_69` +- `"{iv_pct}_{iv_ads}"` => `91_15/11/15` +- `""` -> `Mankey` +![sample](https://cloud.githubusercontent.com/assets/8896778/17285954/0fa44a88-577b-11e6-8204-b1302f4294bd.png) + +## Sniping _(MoveToLocation)_ +### Description +This task will fetch current pokemon spawns from /raw_data of an PokemonGo-Map instance. For information on how to properly setup PokemonGo-Map have a look at the Github page of the project [here](https://github.com/AHAAAAAAA/PokemonGo-Map/). There is an example config in `config/config.json.map.example` + +### Options +* `Address` - Address of the webserver of PokemonGo-Map. ex: `http://localhost:5000` +* `Mode` - Which mode to run snipin on + - `distance` - Will move to the nearest pokemon + - `priority` - Will move to the pokemon with the highest priority assigned (tie breaking by distance) +* `prioritize_vips` - Will prioritize vips in distance and priority mode above all normal pokemon if set to true +* `min_time` - Minimum time the pokemon has to be available before despawn +* `max_distance` - Maximum distance the pokemon is allowed to be when walking, ignored when sniping +* `snipe`: + - `True` - Will teleport to target pokemon, encounter it, teleport back then catch it + - `False` - Will walk normally to the pokemon +* `update_map` - disable/enable if the map location should be automatically updated to the bots current location +* `catch` - A dictionary of pokemon to catch with an assigned priority (higher => better) +* `snipe_high_prio_only` - Whether to snipe pokemon above a certain threshold. +* `snipe_high_prio_threshold` - The threshold number corresponding with the `catch` dictionary. Any pokemon above this threshold will be caught. Other will be igonored. + +#### Example +``` +{ + \\ ... + { + "type": "MoveToMapPokemon", + "config": { + "address": "http://localhost:5000", + "max_distance": 500, + "min_time": 60, + "min_ball": 50, + "prioritize_vips": true, + "snipe": true, + "snipe_high_prio_only": true, + "snipe_high_prio_threshold": 400, + "update_map": true, + "mode": "priority", + "catch": { + "Aerodactyl": 1000, + "Ditto": 900, + "Omastar": 500, + "Omanyte": 150, + "Caterpie": 10, + } + } + } + \\ ... +} +``` diff --git a/docs/develop.md b/docs/develop.md new file mode 100644 index 0000000000..7a1649219f --- /dev/null +++ b/docs/develop.md @@ -0,0 +1,28 @@ +> $ git clone --recursive -b dev https://github.com/PokemonGoF/PokemonGo-Bot +> $ cd PokemonGo-Bot +> // create virtualenv using Python 2.7 executable +> $ virtualenv -p C:\python27\python.exe venv +> $ source venv/Scripts/activate +> $ pip install -r requirements.txt + +Once you are you to date with [dev-branch] (https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) create a pull request and it will be re-viewed + + +### How to add/discover new API +The example is [here](https://github.com/PokemonGoF/PokemonGo-Bot/commit/46e2352ce9f349cc127a408959679282f9999585) +1. Check the type of your API request in [POGOProtos](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/RequestType.proto) For example: RECYCLE_INVENTORY_ITEM +2. Convert to the api call in pokemongo_bot/__init__.py, RECYCLE_INVENTORY_ITEM change to self.api.recycle_inventory_item +``` +def drop_item(self,item_id,count): + self.api.recycle_inventory_item(...............) +``` +3. Where is the param list? +You need check this [Requests/Messages/RecycleInventoryItemMessage.proto](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/Messages/RecycleInventoryItemMessage.proto) +4. Then our final api call is +``` +def drop_item(self,item_id,count): + self.api.recycle_inventory_item(item_id=item_id,count=count) + inventory_req = self.api.call() + print(inventory_req) +``` +5. You can now debug on the log to see if get what you need diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000000..04823a2f5c --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,25 @@ +Start by downloading for your platform: [Mac](https://www.docker.com/products/docker#/mac), [Windows](https://www.docker.com/products/docker#/windows), or [Linux](https://www.docker.com/products/docker#/linux). Once you have Docker installed, simply create the various config.json files for your different accounts (e.g. `configs/config-account1.json`) and then create a Docker image for PokemonGo-Bot using the Dockerfile in this repo. +``` +cd PokemonGo-Bot +docker build -t pokemongo-bot . +``` +You can verify that the image was created with: +``` +docker images +``` + +To run PokemonGo-Bot Docker image you've created, simple run: +``` +docker run --name=pokego-bot1 --rm -it -v $(pwd)/configs/config-account1.json:/usr/src/app/configs/config.json pokemongo-bot +``` +_Check the logs in real-time `docker logs -f pgobot`_ + +If you want to run multiple accounts with the same Docker image, simply specify different config.json and names in the Docker run command. +Do not push your image to a registry with your config.json and account details in it! + +Share web folder with host: +``` +docker run -it -v $(pwd)/web/:/usr/src/app/web --rm --name=pgo-bot-acct1 pokemongo-bot --config config.json +``` + +TODO: Add configuration for running multiple Docker containers from the same image for every bot instance, and a single container for the web UI. diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000000..7720f0a64c --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,49 @@ +### How do I start the application? +After customizing your config.json files, cd to the PokemonGo-Bot folder and enter: +``` +$ python pokecli.py +``` +This will start the application. + +### Python possible bug +If you encounter problems with the module `ssl` and it's function `_create_unverified_context`, just comment it. (Solution available in Python 2.7.11) +In order to comment out the function and the module, please follow the instructions below: +- edit `pokecli.py` +- put `#` before `if` (line 43) and `ssl` (line 44) +- save it + +Please keep in mind that this fix is only necessary if your python version don't have the `_create_unverified_context` argument in the ssl module. + +### What's IV? +Here's the [introduction](http://bulbapedia.bulbagarden.net/wiki/Individual_values) + +### Does it run automatically? +Not yet, still need a trainer to train the script param. But we are very close to. + +### Set GEO Location +It works, use "location": "59.333409,18.045008", in configs/config.json to set lat long for location. Use a Pokemon Go map to find an area with pokemons you still need (e.g. [https://pokevision.com/](https://pokevision.com/)), however don't jump too big distances (see "softban"). + +### Google login issues (Login Error, Server busy)? +Try to generate an [app password](!https://support.google.com/accounts/answer/185833?hl=en) and set is as +``` +-p "" +``` +This error mostly occurs for those who are using 2 factor authentication, but either way, for the purpose of security it would be nice to have a separate password for the bot app. + +### FLEE +The status code "3" corresponds to "Flee" - meaning your Pokemon has ran away. + {"responses": { "CATCH_POKEMON": { "status": 3 } } + +### My pokemon are not showing up in my Pokedex? +Finish the tutorial on a smartphone. This will then allow everything to be visible. + +### How can I maximise my XP per hour? +Quick Tip: When using this script, use a Lucky egg to double the XP for 30 mins. You will level up much faster. A Lucky egg is obtained on level 9 and further on whilst leveling up. (from VipsForever via /r/pokemongodev) + +### How do I use the map?? +[See wiki info here] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page)) + +### No JSON object could be decoded or decoder.py error +If you see "No JSON object could be decoded" or you see "decoder.py" in the last part of the error, this means that there is something wrong with your JSON. + +Copy the json in json files and copy it into http://jsonlint.com/ Then fix the error it gives you in your json. diff --git a/docs/google_map.md b/docs/google_map.md new file mode 100644 index 0000000000..7c6e3aca6c --- /dev/null +++ b/docs/google_map.md @@ -0,0 +1,54 @@ +The webpage is a submodule to this repository and config related to that is in ./web folder + +[OpenPoGoWeb] (https://github.com/OpenPoGo/OpenPoGoWeb) uses Google Maps. Read their [README] (https://github.com/OpenPoGo/OpenPoGoWeb/blob/master/README.md) for how to configure web frontend + +## How to set up a simple webserver with nginx +## SimpleHTTPServer +You can either view the map via opening the html file, or by serving it with SimpleHTTPServer (runs on localhost:8000) +To use SimpleHTTPServer: +``` +$ python -m SimpleHTTPServer [port] +``` +The default port is 8000, you can change that by giving a port number. Anything above port 1000 does not require root. +You will need to set your username(s) in the userdata.js file before opening, **Copy userdata.js.example to userdata.js** and edit with your favorite text editor. Put your username in the quotes instead of "username" +If using multiple usernames format like this +``` +var users = ["username1","username2"]; +``` +On Windows you can now go to http://127.0.0.1:8000 to see the map + + + +### Nginx on Ubuntu 14.x, 16.x +#### 1. Install nginx on your Ubuntu machine (e.g. on locally or AWS) +``` +sudo apt-get update +sudo apt-get install nginx +``` + +#### 2. Check the webserver +Check if the webserver is running by using your browser and entering the IP address of your local machine/server. +On a local machine this would be http://127.0.0.1. On AWS this is your public DNS if you haven't configured an elastic IP. + +#### 3. Change Base Directory of the Webserver +``` +sudo nano "/etc/nginx/sites-enabled/default" +``` +Comment out following line: ```root /var/www/html;``` and change it to the web folder of your PokemonGo-Bot: eg: +``` +/home/user/dev/PokemonGo-Bot/web; +``` +Use `nginx -s reload` to load the new configurations. + + +*** +Common Errors and Solutions + +> missing files: 127.0.0.1 - - "GET /catchable-YOURACCOUNT.json 404 +and location-SOMEACCOUNT.json 404 + +just create the file catachable-someaccount@gmail.com.json and put +``` +{} +``` +save and close repeat for other file. (location-SOMEACCOUNT.json) diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000000..8f5f5692e3 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,93 @@ +### Requirements (click each one for install guide) + +- [Python 2.7.x](http://docs.python-guide.org/en/latest/starting/installation/) +- [pip](https://pip.pypa.io/en/stable/installing/) +- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) +- [docker](https://docs.docker.com/engine/installation/) (Optional) - [how to setup after installation](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/How-to-run-with-Docker) +- [protobuf 3](https://github.com/google/protobuf) (OS Dependent, see below) + +### Protobuf 3 installation + +- OS X: `brew update && brew install --devel protobuf` +- Windows: Download protobuf 3.0: [here](https://github.com/google/protobuf/releases/download/v3.0.0-beta-4/protoc-3.0.0-beta-4-win32.zip) and unzip `bin/protoc.exe` into a folder in your PATH. +- Linux: `apt-get install python-protobuf` + +### Get encrypt.so (Windows part writing need fine tune) +We don't have the copyright of encrypt.so, please grab from internet and build your self.Take the risk as your own. +Example build sequence: +Create a new separate folder some here + +wget http://pgoapi.com/pgoencrypt.tar.gz && tar -xf pgoencrypt.tar.gz && cd pgoencrypt/src/ && make +Then copy libencrypt.so to the gofbot folder and rename to encrypt.so + +### Note on branch +Please keep in mind that master is not always up-to-date whereas 'dev' is. In the installation note below change `master` to `dev` if you want to get and use the latest version. + +## Update +To update your project do (in the project folder): `git pull` + +To update python requirement packages do (in the project folder): `pip install --upgrade -r requirements.txt` + +### Installation Linux +(change master to dev for the latest version) + +``` +$ git clone --recursive -b master https://github.com/PokemonGoF/PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ source bin/activate +$ pip install -r requirements.txt +``` +#### Example Installation for Ubuntu +(change dev to master for the lastest master version) + +http://pastebin.com/pzPjXT65 + + +### Installation Mac +(change master to dev for the latest version) + +``` +$ git clone --recursive -b master https://github.com/PokemonGoF/PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ source bin/activate +$ pip install -r requirements.txt +``` + +### Installation Windows +(change master to dev for the latest version) + +On Windows, you will need to install PyYaml through the installer and not through requirements.txt. + +##### Windows vista, 7, 8: +Go to : http://pyyaml.org/wiki/PyYAML , download the right version for your pc and install it + +##### Windows 10: +Go to [this](http://www.lfd.uci.edu/~gohlke/pythonlibs/#pyyaml) page and download: PyYAML-3.11-cp27-cp27m-win32.whl +(If running 64-bit python or if you get a 'not a supported wheel on this platform' error, +download the 64 bit version instead: PyYAML-3.11-cp27-cp27m-win_amd64.whl ) + +*(Run the following commands from Git Bash.)* + +``` +// switch to the directory where you downloaded PyYAML +$ cd download-directory +// install 32-bit version +$ pip2 install PyYAML-3.11-cp27-cp27m-win32.whl +// if you need to install the 64-bit version, do this instead: +// pip2 install PyYAML-3.11-cp27-cp27m-win_amd64.whl +``` + +After this, just do: + +``` +$ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ script\activate +$ pip2 install -r requirements.txt +$ git submodule init +$ git submodule update +``` diff --git a/docs/pokemon_iv.md b/docs/pokemon_iv.md new file mode 100644 index 0000000000..8662314fd5 --- /dev/null +++ b/docs/pokemon_iv.md @@ -0,0 +1,58 @@ +Individual Values, or IVs function like a Pokémon's "Genes". They are the traits which are passed down from one generation to the next. + +![](http://vignette3.wikia.nocookie.net/pokemon/images/d/dd/ImagesCAD6WL01.jpg/revision/latest?cb=20110511020243) + +**Individual values** + +Every stat has an IV ranging from 0 to 31 for each stat (HP, ATK, DEF, SPA, SPD, and SPE), and at level 100, their IVs are added to the Pokémon's stats for their total values. For example, a level 100 Tyranitar with no Effort Values and 0 IVs has 310 HP, however if it had 31 IVs, it would have 341 HP. + + +These stats are provided randomly for every Pokémon, caught or bred, and although as insignificant as 31 points may seem, they are required for Ace Trainers to obtain when breeding Pokémon with perfect natures/stats. On some occasions they are even the tipping point in a close matchup. For example, if there was a Terrakion with 0 Attack IV, it will have an attack of 358 at level 100 (with an attack improving nature), while a Terrakion with perfect Attack IVs would have 392 Attack. This small difference can mean the difference between a one-hit kill (not an OHKO) and survival with 1 HP. + + +**Breeding IVs** +Fortunately for trainers, Ace Trainers and Pokémon Breeders especially, IVs can be bred to obtain the perfect Pokémon. + +The process of breeding IVs is as follows, the example displayed below is to breed Nidorans: + +* The child's IV's are generated randomly, for example: 7/27/31/14/19/2, in HP/ATK/DEF/SPA/SPD/SPE format. +* Three stats are inherited from the parents, and are selected in three checks: +1. First check: A random stat (HP/ATK/DEF/SPA/SPD/SPE) is selected from either the Mother or the Father and passed on to the child. +2. Second check: A random stat with the exception of HP (ATK/DEF/SPA/SPD/SPE) is selected from either the Mother or the Father and passed on to the child. +3. Third check: A random stat with the exception of HP and DEF (ATK/SPA/SPD/SPE) is selected from either the Mother or the Father and passed on to the child. +This means that HP and DEF are less likely to pass on to the child, however there are ways to make sure the IVs are passed on. + +Letting either one of the parents hold a Power Item can ensure that the Power Item's respective stat will be passed on to the offspring from the parent that holds it. + +If the Power Item called Power Weight (doubles all HP EV gained) is held by a parent with a perfect IV of 31 for HP and the first check selects this parent, the child is ensured to have a perfect IV for HP. The other checks, though, will be random, and either luck or patience is required to eventually get the desired stats. + +Important: Only three stats are inherited per Pokémon, and these can stack. For example, the DEF IV can be inherited from both parents, thus rendering one redundant. + + + +**Checking IVs** +Beginning in Generation III, there has always been an NPC that allows players to check the IVs of their Pokémon. + + +If you wanted to check the IV's yourself the formula is as follows: + +The formula for HP is different from the rest of the stats, so here is the formula for HP: + +> IV=((Stat - Level Value - 10) * 100 / Level Value) - 2 * Base stat - (Math.Floor(EV/4)) + +In layman terms: + +> Individual Value= ((Current Stat Level - Current Level Value - 10) * 100 / Current Level Value) - 2 * Base Stat - (Math.Floor(EV/4)) + +Just in case you don't know (Math.Floor(EV/4)) means to take the amount of EVs you have in HP and divide it by 4 and then round down. +The formula you use for the rest of the stats is the same, so here it is: + +> IV=((Math.Ceiling(Stat/Nature) - 5) * 100 / Level Value) - 2 * Base Stat - (Math.Floor(EV/4)) + +In layman terms: + +> Individual Value= (Math.Ceiling(Current Stat Value/Nature Bonus) * 100 / Current Level Value) - 2 * Base Stat - (Math.Floor(EV/4)) + +Just in case you don't know (Math.Floor(EV/4)) means to take the amount of EVs you have in HP and divide it by 4 and then round down. + +Just in case you don't know (Math.Ceiling(Current Stat Value/Nature Bonus)) means to take the Current Stat Value and divide it by the bonus you get from the Pokémon's nature and then round up. If the stat gets an increase from the nature you divide the Current Stat Value by 1.1, and if it is a decrease from the nature you divide the Current Stat Value by 0.9. From d7cb7df27516b588252a767260176f92586489fa Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Wed, 10 Aug 2016 23:47:32 -0700 Subject: [PATCH 098/143] Dev merge to master, PR (#3564) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adding plugin support (#2679) * Adding plugin support * Adding an empty __init__.py * Moving the base task to the project root (#2702) * Moving the base task to the project root * Moving the base class more * Changing the import again * Adding a heartbeat to the analytics (#2709) * Adding a heartbeat to the analytics * Heartbeat every 30 seconds, not every 5 * Don't double track clients * Fix 'local variable 'bot' referenced before assignment' * Providing an error if tasks don't work for the given api (#2732) * Fix for utf8 encoding when catching lured pokemon (#2720) * Fixing lure pokestop encoding * fixing lure encoding * Fix For catchable not being displayed on the web (#2719) * Fix For catchable not being displayed on the web * Update catch_visible_pokemon.py * Added encrypt.so compilation process to Dockerfile (#2695) * OS Detection for encrypt lib (#2768) Fix 32bit check, darwin and linux use the same file Make it a function Check if file exists, if not show error Define file_name first Fix return Check if file exists, if not show error Print info about paths Fix for 32/64bit detection * Fix Typo in unexpected_response_retry (#2531) fixes #2525 #2523 * Revert "changing license from MIT to GPLv3" This reverts commit 69fb64f2bf7c12e28c2bb6d2b636c6af55822448. * When the google analytics domain is blocked the bot crashed. (#2764) With a simple try / except this can be solved. Fix dirty catch all * Fixes #2698 - Prevents "Possibly searching too often" error after re-login. (#2771) * Fixes #2698 - Added api.activate_signature call to prevent issue after re-login. - Also replaced deprecated log call with event_manager emit to prevent exception being thrown. * Modified to use OS detected library path as per PR #2768 * Support loading plugins from .zip files (#2766) * Keep track of how many pokemon released (#2884) * Setting Library path to work with encrypt.so (#2899) Setting LD_LIBRARY_PATH on Dockerfile * :sparkles: Added login and username to available stats (#2494) Added a player_data property in PokemonGoBot to access player data from outside Added unit tests for login and username stats Added tests for call args when updating the window title Added a platform-specific test for window title updating on win32 platform * [dev] small fixes (#2912) * Fixed emit_event typo * Update CONTRIBUTORS.md * Changed initialization location for "bot" We use bot in main exception on 128 * Update pokecli.py * Rename load_path to load_plugin (#2947) * Adding some logic for pulling plugins from github (#2967) * flush after title update (#2977) * correctly re-raise exception to keep backtrace (#2944) * Update MoveToMapPokemon to use events instead of logger. (#2913) * Config/encrypt.so (#2964) * Add config option for libencrypt.so * Correctly set the config value and check for the file in said dir * Fixed mispelling for "formatted" variable (#2984) * Loading plugins from Github (#2992) * Checking github plugin file existence * Loading plugins from github * Fixed #3000 (#3003) Fixed syntax error on "move_to_map_pokemon.py" that makes the client crash when using this feature. * Added MaxPotion inventory count to summary. (#3015) Short Description: The Max Potion count was missing from the inventory summary. Was #2456 * Added cleanup of download and files for encrypt.so after they are no longer needed (#3011) * Fix bot not returning back after telepoting (#3014) * Fix typo: last_long -> last_lon * Whitespace cleanup * Fix bug introduced by #3037: bot not returning back * Fix Dockerfile installation (#3057) * Fix for #3045 (#3055) * Added request to check configuration (#3089) * Fixed Dockerfile - missing \ on command lines (#3096) * Fixed mispelling for "formatted" variable * Docker commands missing trailing \ * Fix for FileIO slowing bot performance.This puts the map writing into a thread and makes sure it only executes once. (#3100) * Change word usage: "fled" to "escaped" (#3118) "fled" is confusing to lot of people and is easily confused with pokemon vanishing. "escaped" is a better term. * Update the example config file (#3120) * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Add config option for libencrypt.so * Rename path.example.json to path.json.example * typo: logrmation -> information (#2601) Fix a typo. I assume that it was "information" initially, but became "logrmation" when someone used replace all functionality to replace all infos with logs. But I might be totally wrong at this point, idk. Just didn't like the word and wanted to fix that typo. * Change fled to escaped (#3129) Fix an issue after PR #3118 * When JSON parsing fails, give a rough indication of why (#3137) * When JSON parsing fails, give a rough indication of why * Use the official package instead of SHA1 commit * Handle Github Download Zip Format (#3108) * Checking github plugin file existence * Loading plugins from github * Starting install code for github plugins * Updating GithubPlugin to support extracting folders * Handling github zip formats by extracting to the correct location * Refactor catch worker (#2527) * refactor catch worker * fix * few renames * add to contributors * fix * add missing behavior * fix encounter events * don't make events about ignored pokemon * Added Run-Loop (#3143) * Add files via upload modified run script wich let you run the boot in a loop(if it crashes it restarts) * Integreated Loop into run.sh modified run.sh to loop the script so that even if it crashes it automaticly restarts. * fixing loop in spin fort task (#3165) * Some love for the vim users (#3154) * Updated README with link to desktop version (#3208) * Fix for #3190 (#3197) * MoveToMap: Add minimum balls to run (#3166) * added config to ignore item count for Spin and MoveToFort (#3160) * [Inventory Management] Add a central class for caching/parsing inventory & static data (#2528) * new class to centralize inventory management * use new inventory class in evolve_pokemon * use new inventory to display # candy after catch * Keeping a cache of gym information (#3236) * New Option: "dont_nickname_favorite" (#2496) * New Option: "dont_nickname_favorite" This change (line 19) adds the option, that the user can choose, whether their favorite pokemons should also get a new nickname or not. If a user want this, then he or she has to add the line ("dont_nickname_favorite" = true) after ("nickname_template": " ... ",). * Update nickname_pokemon.py * Update * Put change to line 30 This reduce the reduce the runtime, because favorite pokemon won't be added to the list. * Restart the loop when catching pokemon and there are more to catch (#3242) * fixed NameError: global name 'pokemon_name' is not defined (#3244) resolves ```traceback (most recent call last): File "pokecli.py", line 521, in main() File "pokecli.py", line 95, in main bot.tick() File "/usr/src/app/pokemongo_bot/__init__.py", line 451, in tick if worker.work() == WorkerResult.RUNNING: File "/usr/src/app/pokemongo_bot/cell_workers/evolve_pokemon.py", line 38, in work self._execute_pokemon_evolve(pokemon, cache) File "/usr/src/app/pokemongo_bot/cell_workers/evolve_pokemon.py", line 117, in _execute_pokemon_evolve cache[pokemon.name] = 1 NameError: global name 'pokemon_name' is not defined``` * Stop fetching gym details (#3245) * Checking all forts for lured pokemon (#3163) * Fix flooding of keep_best_release (#3223) * Fix flooding of keep_best_release * Fix flooding of keep_best_release * [Feature] Recycle Threshold (#2465) * Add Threshold Option * Add Threshold Option to Example Configs * Add Name to Contributors * Change config name and message * Remove logger * Add option to run when storage less than something * Change Message * Fix * Error fixes, message improvement * Config Changes and Remove Option * Call heartbeat on step_walker even if speed is higher than distance (#2513) * Return an empty list if no pokemon are available. (#3259) The changes introduced in 4c95259 expose this bug. * Allow UpdateTitleStats to emit events instead of rewriting the console (#3264) * Updating our issue and PR templates to be more helpful (#3262) * Dev (#3277) * * adding enhanced sniping capabilities for move_to_map_pokemon * Adding enhanced sniping capabilities for move_to_map_pokemon * Update pgoapi to a newer version (#3241) This should hopefully fix issues like #3181, #3098, #2874 and potentially more. Needs testing/verification. I am running now, but it does take about an hour to trigger. * Fix unexpected egg incubation retry (#3276) incubator['used'] flag is set but not used in IncubateEggs._apply_incubators * has_next_evolution is a function not a property (#3284) * Powerful setup.sh (#3263) * Rewrite run.sh Very powerful run.sh with lots of function. 1.install(make .so) 2.update 3.config generator 4.config backup 5.run loop make it never down It should run like run.sh *.json or other opinion. See -help. * Update run.sh * Update run.sh OK problem solved * Delete setup.py * Rename run.sh to setup.sh * Create run.sh * Update setup.sh * Update install.sh * Update setup.sh * Update run.sh * Update setup.sh Some small fix. * Added +x to run.sh * Added a configuration option "path_startmode" (conflict merge #2489) (#3270) * Upstream update and merge, with path_startmode configuration * Removed logger and fixed base task path * As per request, path_startmode is now path_start_mode * Removed all logging * Adding documentation for how to use and write plugins (#3254) * Adding documentation for how to use and write plugins * Adding a link to the plugins docs in the Readme * Updating link to the plugin docs in the readme * Checking config file exists in run.sh (#3326) * Improve and update pokemon.json (#3331) 1. Unminify for simplier edits 2. Add BaseAttack, BaseDefense, BaseStamina, CaptureRate, FleeRate, Fast/Special Attack(s) * Made paths to .json files absolute so pokecli.py can be called from CRON (#3157) * Made paths to .json files absolute so pokecli.py can be called from CRON * made file paths abs in inventory fixed incorrect dict reference, changed to .get() as felt this was intended * Add fast & charged moves data from #2117 (originally by @iananass) (#3336) Data for pokemon quick & slow attacks * Upgrade pgoapi to the b4bf0e089dfe09903f8dda37dae56910e01f94cc commit(latest for now). (#3337) * Revert "Upgrade pgoapi to the b4bf0e089dfe09903f8dda37dae56910e01f94cc commit…" (#3340) * Added map_path configuration for move_to_map. (#3339) * Log stats on terminal (#3312) * added _log_on_terminal function * Added logic to toggle terminal logging functionality from config file * Added possibility to disable title changes, refactor code * Refactor tuples * Refactor ifs to clearer syntax * changes to improve event system based on new web ui devs requests * typo :D * let's use dict.get a bit to avoid errors * keeping the account in the remote command response * Add ColoredLoggingHandler (#3198) * Update TransferPokemon to use new Inventory Class (#3320) * Update TransferPokemon to use new Inventory Class * Use base_dir * Don't release pokemon if bot is on test mode * Some text fixes for setup.sh (#3390) Minor text fix. * Fix path of shells in install.sh (#3393) * Update install.sh Fix path * Update setup.sh fix path * Update run.sh fix path * Fix evolution error in pokemon.json (#3344) Fix evolution error in pokemon.json * Improve formatting consistency in transfer_pokemon.py (#3397) Improve formatting consistency * Remove unnecessary file * Put info on the next line in run.sh (#3422) * Update setup.sh fix typo * Update run.sh fix typo * Fix Struct() argument 1 must be string, not unicode. (#3375) * Give the possibility to disable a task without removing it (#3417) * Give the possiblity to disable a task in config without removing it from the config file * Put exmple only in nickname task * Add Unit testing * typo * Use enabled false as exemple * fix config creation (#3482) Changed auth to be more specifik and added right permissions. * Remove unused IV calculation from evolve_pokemon (#3487) Previously IV was computed in each worker. Now its fetched from inventory. This was left over and not called in the worker at all. * Don't show Inventory full event if we set "ignore_item_count" (#3440) * Fix showing the date in run.sh (#3433) fix the logic of showing the date * Typo fix: show new catch rate after berry throw. (#3521) * Fix stdout is not a terminal (#3511) * Ensure recycling happens if bag is over capacity. (#3531) Short Description: Ensures you that item Recycling happens if you have more items than the total bag capacity. When you level up, you are awarded items which can cause the bag to be over the capacity. * Better inventory: attacks & movesets, IV CP perfection, pokemon level, etc.. (#3455) * Add "level to CP multiplier" data Data is from justinleewells/pogo-optimizer: https://github.com/justinleewells/pogo-optimizer/blob/edd692d/data/game/level-to-cpm.json * Many improvements & additions for the inventory logic - LevelToCPm, FastAttacks, ChargedAttacks, Movesets - More info for each pokemon: attacks data, percent to max cp, IV CP perfection * Add PyCharm/IDEA *.iml (project file) to ignored * Fixes, improvements & refactoring for inventory.py - Return inadvertently deleted pieces of code (thanks to @achretien) - Evolution logic fixes - Other minor fixes - Moveset logic moved to Moveset class * Fix data for pokemons & charged moves * Inventory tests: pokemon data, LevelToCPm, attacks * Fix travis build * Fix info for Hitmonlee & Hitmonchan * Revert "Better inventory: attacks & movesets, IV CP perfection, pokemon level, etc.." (#3549) * run.bat for windows (#3542) run.bat with persistent loop * Fix error when MoveToFort called from handle_soft_ban.py (#3500) * Fix error when MoveToFort called from handle_soft_ban.py * Added myself to CONTRIBUTORS.md * Fixed list of charged attacks for Venonat (#3548) + BaseDefense/BaseStamina info fix (#3550) * Revert "Revert "Better inventory: attacks & movesets, IV CP perfection, pokemon level, etc.." (#3549)" This reverts commit e9b229ec0fd14a4814ea7431b1256850e907cfbf. * Fix BaseDefense/BaseStamina and type info Fixed BaseDefense/BaseStamina info (was mixed up) Fixed type info for Mr. Mime, Clefairy, Clefable, Jigglypuff, Wigglytuff Added check for correctness of exact CP value calculation * Fixed list of charged attacks for Venonat * Don't kill bot in case of unexpected moveset, use fallback + better error message * Blacklisting tejado from getting mentioned by the mention-bot * Moving wiki pages to docs folder --- .gitignore | 1 + .mention-bot | 3 + CONTRIBUTORS.md | 3 + configs/config.json.cluster.example | 1 + configs/config.json.example | 8 + configs/config.json.map.example | 2 + configs/config.json.path.example | 1 + configs/config.json.pokemon.example | 1 + data/charged_moves.json | 91 + data/fast_moves.json | 41 + data/level_to_cpm.json | 81 + data/pokemon.json | 5901 ++++++++++++++++- docs/auto_restart.md | 73 + docs/configuration_files.md | 271 + docs/develop.md | 28 + docs/docker.md | 25 + docs/faq.md | 49 + docs/google_map.md | 54 + docs/installation.md | 93 + docs/pokemon_iv.md | 58 + install.sh | 10 +- pokecli.py | 11 +- pokemongo_bot/__init__.py | 68 +- pokemongo_bot/base_dir.py | 4 + pokemongo_bot/base_task.py | 1 + .../cell_workers/catch_visible_pokemon.py | 4 +- pokemongo_bot/cell_workers/evolve_pokemon.py | 5 - pokemongo_bot/cell_workers/move_to_fort.py | 12 +- .../cell_workers/move_to_map_pokemon.py | 8 +- .../cell_workers/pokemon_catch_worker.py | 20 +- pokemongo_bot/cell_workers/recycle_items.py | 5 +- pokemongo_bot/cell_workers/spin_fort.py | 11 +- .../cell_workers/transfer_pokemon.py | 204 +- .../cell_workers/update_title_stats.py | 53 +- pokemongo_bot/event_handlers/__init__.py | 1 + .../event_handlers/colored_logging_handler.py | 172 + pokemongo_bot/inventory.py | 704 +- pokemongo_bot/socketio_server/app.py | 8 +- pokemongo_bot/step_walker.py | 11 + pokemongo_bot/tree_config_builder.py | 3 +- pokemongo_bot/websocket_remote_control.py | 9 +- run.bat | 10 + run.sh | 13 +- setup.sh | 23 +- tests/inventory_test.py | 183 + tests/tree_config_builder_test.py | 19 + 46 files changed, 8087 insertions(+), 270 deletions(-) create mode 100644 .mention-bot create mode 100644 data/charged_moves.json create mode 100644 data/fast_moves.json create mode 100644 data/level_to_cpm.json create mode 100644 docs/auto_restart.md create mode 100644 docs/configuration_files.md create mode 100644 docs/develop.md create mode 100644 docs/docker.md create mode 100644 docs/faq.md create mode 100644 docs/google_map.md create mode 100644 docs/installation.md create mode 100644 docs/pokemon_iv.md create mode 100644 pokemongo_bot/base_dir.py create mode 100644 pokemongo_bot/event_handlers/colored_logging_handler.py create mode 100644 run.bat create mode 100644 tests/inventory_test.py diff --git a/.gitignore b/.gitignore index 4721ce0253..06973c1249 100644 --- a/.gitignore +++ b/.gitignore @@ -100,6 +100,7 @@ share/ # PyCharm IDE settings .idea/ +*.iml # Personal load details src/ diff --git a/.mention-bot b/.mention-bot new file mode 100644 index 0000000000..5635897ab2 --- /dev/null +++ b/.mention-bot @@ -0,0 +1,3 @@ +{ + "userBlacklist": ["tejado"] +} diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 57b289ab90..fc2478f20c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -59,3 +59,6 @@ * nikhil-pandey * thebigjc * JaapMoolenaar + * eevee-github + * g0vanish + * cmezh diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index e4597519f6..eb507cd43c 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -81,6 +81,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.example b/configs/config.json.example index d2e8d4f064..59974ba156 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -21,6 +21,13 @@ { "type": "TransferPokemon" }, + { + "type": "NicknamePokemon", + "config": { + "enabled": false, + "nickname_template": "{iv_pct}_{iv_ads}" + } + }, { "type": "EvolvePokemon", "config": { @@ -89,6 +96,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.map.example b/configs/config.json.map.example index bb6878f5ac..cf6604976b 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -66,6 +66,7 @@ "snipe_high_prio_threshold": 400, "update_map": true, "mode": "priority", + "map_path": "raw_data", "catch": { "==========Legendaries==========": 0, "Aerodactyl": 1000, @@ -322,6 +323,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 6f7b04c305..6b6619573b 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -83,6 +83,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index e7ba38dc37..1dfa01199d 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -89,6 +89,7 @@ "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or" }, diff --git a/data/charged_moves.json b/data/charged_moves.json new file mode 100644 index 0000000000..c3f993191c --- /dev/null +++ b/data/charged_moves.json @@ -0,0 +1,91 @@ +[{"id":32,"name":"Stone Edge","type":"Rock","damage":80,"duration":3100,"energy":100,"dps":25.8}, +{"id":28,"name":"Cross Chop","type":"Fighting","damage":60,"duration":2000,"energy":100,"dps":30.0}, +{"id":83,"name":"Dragon Claw","type":"Dragon","damage":35,"duration":1500,"energy":50,"dps":23.33}, +{"id":40,"name":"Blizzard","type":"Ice","damage":100,"duration":3900,"energy":100,"dps":25.64}, +{"id":131,"name":"Body Slam","type":"Normal","damage":40,"duration":1560,"energy":50,"dps":25.64}, +{"id":22,"name":"Megahorn","type":"Bug","damage":80,"duration":3200,"energy":100,"dps":25.0}, +{"id":122,"name":"Hurricane","type":"Flying","damage":80,"duration":3200,"energy":100,"dps":25.0}, +{"id":116,"name":"Solar Beam","type":"Grass","damage":120,"duration":4900,"energy":100,"dps":24.48}, +{"id":103,"name":"Fire Blast","type":"Fire","damage":100,"duration":4100,"energy":100,"dps":24.39}, +{"id":14,"name":"Hyper Beam","type":"Normal","damage":120,"duration":5000,"energy":100,"dps":24.0}, +{"id":31,"name":"Earthquake","type":"Ground","damage":100,"duration":4200,"energy":100,"dps":23.8}, +{"id":118,"name":"Power Whip","type":"Grass","damage":70,"duration":2800,"energy":100,"dps":25.0}, +{"id":107,"name":"Hydro Pump","type":"Water","damage":90,"duration":3800,"energy":100,"dps":23.68}, +{"id":117,"name":"Leaf Blade","type":"Grass","damage":55,"duration":2800,"energy":50,"dps":19.64}, +{"id":78,"name":"Thunder","type":"Electric","damage":100,"duration":4300,"energy":100,"dps":23.25}, +{"id":123,"name":"Brick Break","type":"Fighting","damage":30,"duration":1600,"energy":33,"dps":18.75}, +{"id":92,"name":"Gunk Shot","type":"Poison","damage":65,"duration":3000,"energy":100,"dps":21.66}, +{"id":90,"name":"Sludge Bomb","type":"Poison","damage":55,"duration":2600,"energy":50,"dps":21.15}, +{"id":42,"name":"Heat Wave","type":"Fire","damage":80,"duration":3800,"energy":100,"dps":21.05}, +{"id":87,"name":"Moonblast","type":"Fairy","damage":85,"duration":4100,"energy":100,"dps":20.73}, +{"id":91,"name":"Sludge Wave","type":"Poison","damage":70,"duration":3400,"energy":100,"dps":20.58}, +{"id":79,"name":"Thunderbolt","type":"Electric","damage":55,"duration":2700,"energy":50,"dps":20.37}, +{"id":47,"name":"Petal Blizzard","type":"Grass","damage":65,"duration":3200,"energy":50,"dps":20.31}, +{"id":89,"name":"Cross Poison","type":"Poison","damage":25,"duration":1500,"energy":25,"dps":16.66}, +{"id":108,"name":"Psychic","type":"Psychic","damage":55,"duration":2800,"energy":50,"dps":19.64}, +{"id":58,"name":"Aqua Tail","type":"Water","damage":45,"duration":2350,"energy":50,"dps":19.14}, +{"id":24,"name":"Flamethrower","type":"Fire","damage":55,"duration":2900,"energy":50,"dps":18.96}, +{"id":88,"name":"Play Rough","type":"Fairy","damage":55,"duration":2900,"energy":50,"dps":18.96}, +{"id":82,"name":"Dragon Pulse","type":"Dragon","damage":65,"duration":3600,"energy":50,"dps":18.05}, +{"id":39,"name":"Ice Beam","type":"Ice","damage":65,"duration":3650,"energy":50,"dps":17.8}, +{"id":49,"name":"Bug Buzz","type":"Bug","damage":75,"duration":4250,"energy":50,"dps":17.64}, +{"id":46,"name":"Drill Run","type":"Ground","damage":50,"duration":3400,"energy":33,"dps":14.7}, +{"id":59,"name":"Seed Bomb","type":"Grass","damage":40,"duration":2400,"energy":33,"dps":16.66}, +{"id":77,"name":"Thunder Punch","type":"Electric","damage":40,"duration":2400,"energy":33,"dps":16.66}, +{"id":100,"name":"X Scissor","type":"Bug","damage":35,"duration":2100,"energy":33,"dps":16.66}, +{"id":129,"name":"Hyper Fang","type":"Normal","damage":35,"duration":2100,"energy":33,"dps":16.66}, +{"id":64,"name":"Rock Slide","type":"Rock","damage":50,"duration":3200,"energy":33,"dps":15.62}, +{"id":94,"name":"Bone Club","type":"Ground","damage":25,"duration":1600,"energy":25,"dps":15.62}, +{"id":36,"name":"Flash Cannon","type":"Steel","damage":60,"duration":3900,"energy":33,"dps":15.38}, +{"id":74,"name":"Iron Head","type":"Steel","damage":30,"duration":2000,"energy":33,"dps":15.0}, +{"id":38,"name":"Drill Peck","type":"Flying","damage":40,"duration":2700,"energy":33,"dps":14.81}, +{"id":60,"name":"Psyshock","type":"Psychic","damage":40,"duration":2700,"energy":33,"dps":14.81}, +{"id":70,"name":"Shadow Ball","type":"Ghost","damage":45,"duration":3080,"energy":33,"dps":14.61}, +{"id":99,"name":"Signal Beam","type":"Bug","damage":45,"duration":3100,"energy":33,"dps":14.51}, +{"id":115,"name":"Fire Punch","type":"Fire","damage":40,"duration":2800,"energy":33,"dps":14.28}, +{"id":54,"name":"Submission","type":"Fighting","damage":30,"duration":2100,"energy":33,"dps":14.28}, +{"id":102,"name":"Flame Burst","type":"Fire","damage":30,"duration":2100,"energy":25,"dps":14.28}, +{"id":127,"name":"Stomp","type":"Normal","damage":30,"duration":2100,"energy":25,"dps":14.28}, +{"id":35,"name":"Discharge","type":"Electric","damage":35,"duration":2500,"energy":33,"dps":14.0}, +{"id":65,"name":"Power Gem","type":"Rock","damage":40,"duration":2900,"energy":33,"dps":13.79}, +{"id":106,"name":"Scald","type":"Water","damage":55,"duration":4000,"energy":33,"dps":13.75}, +{"id":109,"name":"Psystrike","type":"Psychic","damage":70,"duration":5100,"energy":100,"dps":13.72}, +{"id":56,"name":"Low Sweep","type":"Fighting","damage":30,"duration":2250,"energy":25,"dps":13.33}, +{"id":51,"name":"Night Slash","type":"Dark","damage":30,"duration":2700,"energy":25,"dps":11.11}, +{"id":86,"name":"Dazzling Gleam","type":"Fairy","damage":55,"duration":4200,"energy":33,"dps":13.09}, +{"id":16,"name":"Dark Pulse","type":"Dark","damage":45,"duration":3500,"energy":33,"dps":12.85}, +{"id":33,"name":"Ice Punch","type":"Ice","damage":45,"duration":3500,"energy":33,"dps":12.85}, +{"id":26,"name":"Dig","type":"Ground","damage":70,"duration":5800,"energy":33,"dps":12.06}, +{"id":20,"name":"Vice Grip","type":"Normal","damage":25,"duration":2100,"energy":20,"dps":11.9}, +{"id":18,"name":"Sludge","type":"Poison","damage":30,"duration":2600,"energy":25,"dps":11.53}, +{"id":96,"name":"Mud Bomb","type":"Ground","damage":30,"duration":2600,"energy":25,"dps":11.53}, +{"id":126,"name":"Horn Attack","type":"Normal","damage":25,"duration":2200,"energy":25,"dps":11.36}, +{"id":121,"name":"Air Cutter","type":"Flying","damage":30,"duration":3300,"energy":25,"dps":9.09}, +{"id":132,"name":"Rest","type":"Normal","damage":35,"duration":3100,"energy":33,"dps":11.29}, +{"id":72,"name":"Magnet Bomb","type":"Steel","damage":30,"duration":2800,"energy":25,"dps":10.71}, +{"id":57,"name":"Aqua Jet","type":"Water","damage":25,"duration":2350,"energy":20,"dps":10.63}, +{"id":105,"name":"Water Pulse","type":"Water","damage":35,"duration":3300,"energy":25,"dps":10.6}, +{"id":30,"name":"Psybeam","type":"Psychic","damage":40,"duration":3800,"energy":25,"dps":10.52}, +{"id":63,"name":"Rock Tomb","type":"Rock","damage":30,"duration":3400,"energy":25,"dps":8.82}, +{"id":50,"name":"Poison Fang","type":"Poison","damage":25,"duration":2400,"energy":20,"dps":10.41}, +{"id":104,"name":"Brine","type":"Water","damage":25,"duration":2400,"energy":25,"dps":10.41}, +{"id":45,"name":"Aerial Ace","type":"Flying","damage":30,"duration":2900,"energy":25,"dps":10.34}, +{"id":53,"name":"Bubble Beam","type":"Water","damage":30,"duration":2900,"energy":25,"dps":10.34}, +{"id":95,"name":"Bulldoze","type":"Ground","damage":35,"duration":3400,"energy":25,"dps":10.29}, +{"id":125,"name":"Swift","type":"Normal","damage":30,"duration":3000,"energy":25,"dps":10.0}, +{"id":62,"name":"Ancient Power","type":"Rock","damage":35,"duration":3600,"energy":25,"dps":9.72}, +{"id":114,"name":"Giga Drain","type":"Grass","damage":35,"duration":3600,"energy":33,"dps":9.72}, +{"id":69,"name":"Ominous Wind","type":"Ghost","damage":30,"duration":3100,"energy":25,"dps":9.67}, +{"id":67,"name":"Shadow Punch","type":"Ghost","damage":20,"duration":2100,"energy":25,"dps":9.52}, +{"id":80,"name":"Twister","type":"Dragon","damage":25,"duration":2700,"energy":20,"dps":9.25}, +{"id":85,"name":"Draining Kiss","type":"Fairy","damage":25,"duration":2800,"energy":20,"dps":8.92}, +{"id":21,"name":"Flame Wheel","type":"Fire","damage":40,"duration":4600,"energy":25,"dps":8.69}, +{"id":133,"name":"Struggle","type":"Normal","damage":15,"duration":1695,"energy":20,"dps":8.84}, +{"id":101,"name":"Flame Charge","type":"Fire","damage":25,"duration":3100,"energy":20,"dps":8.06}, +{"id":34,"name":"Heart Stamp","type":"Psychic","damage":20,"duration":2550,"energy":25,"dps":7.84}, +{"id":75,"name":"Parabolic Charge","type":"Electric","damage":15,"duration":2100,"energy":20,"dps":7.14}, +{"id":111,"name":"Icy Wind","type":"Ice","damage":25,"duration":3800,"energy":20,"dps":6.57}, +{"id":84,"name":"Disarming Voice","type":"Fairy","damage":25,"duration":3900,"energy":20,"dps":6.41}, +{"id":13,"name":"Wrap","type":"Normal","damage":25,"duration":4000,"energy":20,"dps":6.25}, +{"id":66,"name":"Shadow Sneak","type":"Ghost","damage":15,"duration":3100,"energy":20,"dps":4.83}, +{"id":48,"name":"Mega Drain","type":"Grass","damage":15,"duration":3200,"energy":20,"dps":4.68}] diff --git a/data/fast_moves.json b/data/fast_moves.json new file mode 100644 index 0000000000..ec16d2cc8a --- /dev/null +++ b/data/fast_moves.json @@ -0,0 +1,41 @@ +[{"id":222,"name":"Pound","type":"Normal","damage":7,"duration":540,"energy":7,"dps":12.96}, +{"id":228,"name":"Metal Claw","type":"Steel","damage":8,"duration":630,"energy":7,"dps":12.69}, +{"id":226,"name":"Psycho Cut","type":"Psychic","damage":7,"duration":570,"energy":7,"dps":12.28}, +{"id":210,"name":"Wing Attack","type":"Flying","damage":9,"duration":750,"energy":7,"dps":12.0}, +{"id":202,"name":"Bite","type":"Dark","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":204,"name":"Dragon Breath","type":"Dragon","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":220,"name":"Scratch","type":"Normal","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":230,"name":"Water Gun","type":"Water","damage":6,"duration":500,"energy":7,"dps":12.0}, +{"id":240,"name":"Fire Fang","type":"Fire","damage":10,"duration":840,"energy":4,"dps":11.9}, +{"id":213,"name":"Shadow Claw","type":"Ghost","damage":11,"duration":950,"energy":7,"dps":11.57}, +{"id":238,"name":"Feint Attack","type":"Dark","damage":12,"duration":1040,"energy":7,"dps":11.53}, +{"id":224,"name":"Poison Jab","type":"Poison","damage":12,"duration":1050,"energy":7,"dps":11.42}, +{"id":234,"name":"Zen Headbutt","type":"Psychic","damage":12,"duration":1050,"energy":4,"dps":11.42}, +{"id":239,"name":"Steel Wing","type":"Steel","damage":15,"duration":1330,"energy":4,"dps":11.27}, +{"id":201,"name":"Bug Bite","type":"Bug","damage":5,"duration":450,"energy":7,"dps":11.11}, +{"id":218,"name":"Frost Breath","type":"Ice","damage":9,"duration":810,"energy":7,"dps":11.11}, +{"id":233,"name":"Mud Slap","type":"Ground","damage":15,"duration":1350,"energy":9,"dps":11.11}, +{"id":216,"name":"Mud Shot","type":"Ground","damage":6,"duration":550,"energy":7,"dps":10.9}, +{"id":221,"name":"Tackle","type":"Normal","damage":12,"duration":1100,"energy":7,"dps":10.9}, +{"id":237,"name":"Bubble","type":"Water","damage":25,"duration":2300,"energy":15,"dps":10.86}, +{"id":214,"name":"Vine Whip","type":"Grass","damage":7,"duration":650,"energy":7,"dps":10.76}, +{"id":217,"name":"Ice Shard","type":"Ice","damage":15,"duration":1400,"energy":7,"dps":10.71}, +{"id":241,"name":"Rock Smash","type":"Fighting","damage":15,"duration":1410,"energy":7,"dps":10.63}, +{"id":223,"name":"Cut","type":"Normal","damage":12,"duration":1130,"energy":7,"dps":10.61}, +{"id":236,"name":"Poison Sting","type":"Poison","damage":6,"duration":575,"energy":4,"dps":10.43}, +{"id":215,"name":"Razor Leaf","type":"Grass","damage":15,"duration":1450,"energy":7,"dps":10.34}, +{"id":212,"name":"Lick","type":"Ghost","damage":5,"duration":500,"energy":7,"dps":10.0}, +{"id":206,"name":"Spark","type":"Electric","damage":7,"duration":700,"energy":4,"dps":10.0}, +{"id":203,"name":"Sucker Punch","type":"Dark","damage":7,"duration":700,"energy":4,"dps":10.0}, +{"id":235,"name":"Confusion","type":"Psychic","damage":15,"duration":1510,"energy":7,"dps":9.93}, +{"id":225,"name":"Acid","type":"Poison","damage":10,"duration":1050,"energy":7,"dps":9.52}, +{"id":209,"name":"Ember","type":"Fire","damage":10,"duration":1050,"energy":7,"dps":9.52}, +{"id":227,"name":"Rock Throw","type":"Rock","damage":12,"duration":1360,"energy":7,"dps":8.82}, +{"id":211,"name":"Peck","type":"Flying","damage":10,"duration":1150,"energy":10,"dps":8.69}, +{"id":207,"name":"Low Kick","type":"Fighting","damage":5,"duration":600,"energy":7,"dps":8.33}, +{"id":205,"name":"Thunder Shock","type":"Electric","damage":5,"duration":600,"energy":7,"dps":8.33}, +{"id":229,"name":"Bullet Punch","type":"Steel","damage":10,"duration":1200,"energy":7,"dps":8.33}, +{"id":219,"name":"Quick Attack","type":"Normal","damage":10,"duration":1330,"energy":7,"dps":7.51}, +{"id":200,"name":"Fury Cutter","type":"Bug","damage":3,"duration":400,"energy":12,"dps":7.5}, +{"id":208,"name":"Karate Chop","type":"Fighting","damage":6,"duration":800,"energy":7,"dps":7.5}, +{"id":231,"name":"Splash","type":"Water","damage":0,"duration":1230,"energy":7,"dps":0.0}] \ No newline at end of file diff --git a/data/level_to_cpm.json b/data/level_to_cpm.json new file mode 100644 index 0000000000..d2483d9a41 --- /dev/null +++ b/data/level_to_cpm.json @@ -0,0 +1,81 @@ +{ + "1": 0.094, + "1.5": 0.135137432, + "2": 0.16639787, + "2.5": 0.192650919, + "3": 0.21573247, + "3.5": 0.236572661, + "4": 0.25572005, + "4.5": 0.273530381, + "5": 0.29024988, + "5.5": 0.306057377, + "6": 0.3210876, + "6.5": 0.335445036, + "7": 0.34921268, + "7.5": 0.362457751, + "8": 0.37523559, + "8.5": 0.387592406, + "9": 0.39956728, + "9.5": 0.411193551, + "10": 0.42250001, + "10.5": 0.432926419, + "11": 0.44310755, + "11.5": 0.4530599578, + "12": 0.46279839, + "12.5": 0.472336083, + "13": 0.48168495, + "13.5": 0.4908558, + "14": 0.49985844, + "14.5": 0.508701765, + "15": 0.51739395, + "15.5": 0.525942511, + "16": 0.53435433, + "16.5": 0.542635767, + "17": 0.55079269, + "17.5": 0.558830576, + "18": 0.56675452, + "18.5": 0.574569153, + "19": 0.58227891, + "19.5": 0.589887917, + "20": 0.59740001, + "20.5": 0.604818814, + "21": 0.61215729, + "21.5": 0.619399365, + "22": 0.62656713, + "22.5": 0.633644533, + "23": 0.64065295, + "23.5": 0.647576426, + "24": 0.65443563, + "24.5": 0.661214806, + "25": 0.667934, + "25.5": 0.674577537, + "26": 0.68116492, + "26.5": 0.687680648, + "27": 0.69414365, + "27.5": 0.700538673, + "28": 0.70688421, + "28.5": 0.713164996, + "29": 0.71939909, + "29.5": 0.725571552, + "30": 0.7317, + "30.5": 0.734741009, + "31": 0.73776948, + "31.5": 0.740785574, + "32": 0.74378943, + "32.5": 0.746781211, + "33": 0.74976104, + "33.5": 0.752729087, + "34": 0.75568551, + "34.5": 0.758630378, + "35": 0.76156384, + "35.5": 0.764486065, + "36": 0.76739717, + "36.5": 0.770297266, + "37": 0.7731865, + "37.5": 0.776064962, + "38": 0.77893275, + "38.5": 0.781790055, + "39": 0.78463697, + "39.5": 0.787473578, + "40": 0.79030001 +} \ No newline at end of file diff --git a/data/pokemon.json b/data/pokemon.json index a227106841..44a22a9fd0 100644 --- a/data/pokemon.json +++ b/data/pokemon.json @@ -1 +1,5900 @@ -[{"Number":"001","Name":"Bulbasaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Tackle","Vine Whip"],"Weight":"6.9 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"002","Name":"Ivysaur"},{"Number":"003","Name":"Venusaur"}]},{"Number":"002","Name":"Ivysaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"13.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"003","Name":"Venusaur"}]},{"Number":"003","Name":"Venusaur","Classification":"Seed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"100.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"},{"Number":"002","Name":"Ivysaur"}]},{"Number":"004","Name":"Charmander","Classification":"Lizard Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Scratch"],"Weight":"8.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"005","Name":"Charmeleon"},{"Number":"006","Name":"Charizard"}]},{"Number":"005","Name":"Charmeleon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"19.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"}],"Next Evolution Requirements":{"Amount":100,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"006","Name":"Charizard"}]},{"Number":"006","Name":"Charizard","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Ember","Wing Attack"],"Weight":"90.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"},{"Number":"005","Name":"Charmeleon"}]},{"Number":"007","Name":"Squirtle","Classification":"Tiny Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Tackle","Bubble"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"008","Name":"Wartortle"},{"Number":"009","Name":"Blastoise"}]},{"Number":"008","Name":"Wartortle","Classification":"Turtle Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"22.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"}],"Next Evolution Requirements":{"Amount":100,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"009","Name":"Blastoise"}]},{"Number":"009","Name":"Blastoise","Classification":"Shellfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"85.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"},{"Number":"008","Name":"Wartortle"}]},{"Number":"010","Name":"Caterpie","Classification":"Worm Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"2.9 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"011","Name":"Metapod"},{"Number":"012","Name":"Butterfree"}]},{"Number":"011","Name":"Metapod","Classification":"Cocoon Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"9.9 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"}],"Next Evolution Requirements":{"Amount":50,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"012","Name":"Butterfree"}]},{"Number":"012","Name":"Butterfree","Classification":"Butterfly Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"32.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"},{"Number":"011","Name":"Metapod"}]},{"Number":"013","Name":"Weedle","Classification":"Hairy Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Sting"],"Weight":"3.2 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"014","Name":"Kakuna"},{"Number":"015","Name":"Beedrill"}]},{"Number":"014","Name":"Kakuna","Classification":"Cocoon Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Posion Sting"],"Weight":"10.0 kg","Height":"0.6 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"}],"Next Evolution Requirements":{"Amount":50,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"015","Name":"Beedrill"}]},{"Number":"015","Name":"Beedrill","Classification":"Poison Bee Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Jab"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"},{"Number":"014","Name":"Kakuna"}]},{"Number":"016","Name":"Pidgey","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"1.8 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"017","Name":"Pidgeotto"},{"Number":"018","Name":"Pidgeot"}]},{"Number":"017","Name":"Pidgeotto","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"30.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"}],"Next Evolution Requirements":{"Amount":50,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"018","Name":"Pidgeot"}]},{"Number":"018","Name":"Pidgeot","Classification":"Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Hurricane"],"Weight":"39.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"},{"Number":"017","Name":"Pidgeotto"}]},{"Number":"019","Name":"Rattata","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Body Slam","Dig","Hyper Fang"],"Weight":"3.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Rattata candies"},"Next evolution(s)":[{"Number":"020","Name":"Raticate"}]},{"Number":"020","Name":"Raticate","Classification":"Mouse Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Quick Attack"],"Special Attack(s)":["Dig","Hyper Beam","Hyper Fang"],"Weight":"18.5 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"019","Name":"Rattata"}]},{"Number":"021","Name":"Spearow","Classification":"Tiny Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"2.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Spearow candies"},"Next evolution(s)":[{"Number":"022","Name":"Fearow"}]},{"Number":"022","Name":"Fearow","Classification":"Beak Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Steel Wing"],"Weight":"38.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"021","Name":"Spearow"}]},{"Number":"023","Name":"Ekans","Classification":"Snake Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Sting"],"Weight":"6.9 kg","Height":"2.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ekans candies"},"Next evolution(s)":[{"Number":"024","Name":"Arbok"}]},{"Number":"024","Name":"Arbok","Classification":"Cobra Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Bite"],"Weight":"65.0 kg","Height":"3.5 m","Previous evolution(s)":[{"Number":"023","Name":"Ekans"}]},{"Number":"025","Name":"Pikachu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Quick Attack","Thunder Shock"],"Weight":"6.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Pikachu candies"},"Next evolution(s)":[{"Number":"026","Name":"Raichu"}]},{"Number":"026","Name":"Raichu","Classification":"Mouse Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock","Spark"],"Weight":"30.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"025","Name":"Pikachu"}]},{"Number":"027","Name":"Sandshrew","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"12.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Sandshrew candies"},"Next evolution(s)":[{"Number":"028","Name":"Sandslash"}]},{"Number":"028","Name":"Sandslash","Classification":"Mouse Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"027","Name":"Sandshrew"}]},{"Number":"029","Name":"Nidoran F","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"7.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"030","Name":"Nidorina"},{"Number":"031","Name":"Nidoqueen"}]},{"Number":"030","Name":"Nidorina","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"20.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"031","Name":"Nidoqueen"}]},{"Number":"031","Name":"Nidoqueen","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"},{"Number":"030","Name":"Nidorina"}]},{"Number":"032","Name":"Nidoran M","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Peck","Poison Sting"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"033","Name":"Nidorino"},{"Number":"034","Name":"Nidoking"}]},{"Number":"033","Name":"Nidorino","Classification":"Poison Pin Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"19.5 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran M candies"},"Next evolution(s)":[{"Number":"034","Name":"Nidoking"}]},{"Number":"034","Name":"Nidoking","Classification":"Drill Pokemon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Fury Cutter","Poison Jab"],"Weight":"62.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"},{"Number":"033","Name":"Nidorino"}]},{"Number":"035","Name":"Clefairy","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"7.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Clefairy candies"},"Next evolution(s)":[{"Number":"036","Name":"Clefable"}]},{"Number":"036","Name":"Clefable","Classification":"Fairy Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"40.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"035","Name":"Clefairy"}]},{"Number":"037","Name":"Vulpix","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"9.9 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Vulpix candies"},"Next evolution(s)":[{"Number":"038","Name":"Ninetales"}]},{"Number":"038","Name":"Ninetales","Classification":"Fox Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"19.9 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"037","Name":"Vulpix"}]},{"Number":"039","Name":"Jigglypuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"5.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Jigglypuff candies"},"Next evolution(s)":[{"Number":"039","Name":"Jigglypuff"}]},{"Number":"040","Name":"Wigglytuff","Classification":"Balloon Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"12.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"040","Name":"Wigglytuff"}]},{"Number":"041","Name":"Zubat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Quick Attack"],"Weight":"7.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Zubat candies"},"Next evolution(s)":[{"Number":"042","Name":"Golbat"}]},{"Number":"042","Name":"Golbat","Classification":"Bat Pokemon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Wing Attack"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"041","Name":"Zubat"}]},{"Number":"043","Name":"Oddish","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"5.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"044","Name":"Gloom"},{"Number":"045","Name":"Vileplume"}]},{"Number":"044","Name":"Gloom","Classification":"Weed Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"8.6 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"}],"Next Evolution Requirements":{"Amount":100,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"045","Name":"Vileplume"}]},{"Number":"045","Name":"Vileplume","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid",""],"Weight":"18.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"},{"Number":"044","Name":"Gloom"}]},{"Number":"046","Name":"Paras","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Scratch"],"Weight":"5.4 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Paras candies"},"Next evolution(s)":[{"Number":"047","Name":"Parasect"}]},{"Number":"047","Name":"Parasect","Classification":"Mushroom Pokemon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Fury Cutter"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"046","Name":"Paras"}]},{"Number":"048","Name":"Venonat","Classification":"Insect Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Venonat candies"},"Next evolution(s)":[{"Number":"049","Name":"Venomoth"}]},{"Number":"049","Name":"Venomoth","Classification":"Poison Moth Pokemon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"12.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"048","Name":"Venonat"}]},{"Number":"050","Name":"Diglett","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"0.8 kg","Height":"0.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Diglett candies"},"Next evolution(s)":[{"Number":"051","Name":"Dugtrio"}]},{"Number":"051","Name":"Dugtrio","Classification":"Mole Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Sucker Punch"],"Weight":"33.3 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"050","Name":"Diglett"}]},{"Number":"052","Name":"Meowth","Classification":"Scratch Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Scratch"],"Weight":"4.2 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Meowth candies"},"Next evolution(s)":[{"Number":"053","Name":"Persian"}]},{"Number":"053","Name":"Persian","Classification":"Classy Cat Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Scratch"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"052","Name":"Meowth"}]},{"Number":"054","Name":"Psyduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun","Zen Headbutt"],"Weight":"19.6 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Psyduck candies"},"Next evolution(s)":[{"Number":"055","Name":"Golduck"}]},{"Number":"055","Name":"Golduck","Classification":"Duck Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"76.6 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"054","Name":"Psyduck"}]},{"Number":"056","Name":"Mankey","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Scratch"],"Weight":"28.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Mankey candies"},"Next evolution(s)":[{"Number":"057","Name":"Primeape"}]},{"Number":"057","Name":"Primeape","Classification":"Pig Monkey Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"056","Name":"Mankey"}]},{"Number":"058","Name":"Growlithe","Classification":"Puppy Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Ember"],"Weight":"19.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":50,"Name":"Growlithe candies"},"Next evolution(s)":[{"Number":"059","Name":"Arcanine"}]},{"Number":"059","Name":"Arcanine","Classification":"Legendary Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Fire Fang"],"Weight":"155.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"058","Name":"Growlithe"}]},{"Number":"060","Name":"Poliwag","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"12.4 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"061","Name":"Poliwhirl"},{"Number":"062","Name":"Poliwrath"}]},{"Number":"061","Name":"Poliwhirl","Classification":"Tadpole Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"20.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"}],"Next Evolution Requirements":{"Amount":100,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"062","Name":"Poliwrath"}]},{"Number":"062","Name":"Poliwrath","Classification":"Tadpole Pokemon","Type I":["Water"],"Type II":["Fighting"],"Weaknesses":["Electric","Grass","Flying","Psychic","Fairy"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"54.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"},{"Number":"061","Name":"Poliwhirl"}]},{"Number":"063","Name":"Abra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Zen Headbutt",""],"Weight":"19.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":25,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"064","Name":"Kadabra"},{"Number":"065","Name":"Alakazam"}]},{"Number":"064","Name":"Kadabra","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"56.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"}],"Next Evolution Requirements":{"Amount":100,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"065","Name":"Alakazam"}]},{"Number":"065","Name":"Alakazam","Classification":"Psi Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"48.0 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"},{"Number":"064","Name":"Kadabra"}]},{"Number":"066","Name":"Machop","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"19.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"067","Name":"Machoke"},{"Number":"068","Name":"Machamp"}]},{"Number":"067","Name":"Machoke","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"70.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"}],"Next Evolution Requirements":{"Amount":100,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"068","Name":"Machamp"}]},{"Number":"068","Name":"Machamp","Classification":"Superpower Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Karate Chop"],"Weight":"130.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"},{"Number":"067","Name":"Machoke"}]},{"Number":"069","Name":"Bellsprout","Classification":"Flower Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Vine Whip"],"Weight":"4.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"070","Name":"Weepinbell"},{"Number":"071","Name":"Victreebel"}]},{"Number":"070","Name":"Weepinbell","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"6.4 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"071","Name":"Victreebel"}]},{"Number":"071","Name":"Victreebel","Classification":"Flycatcher Pokemon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"15.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"},{"Number":"070","Name":"Weepinbell"}]},{"Number":"072","Name":"Tentacool","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Bubble","Poison Sting"],"Weight":"45.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Tentacool candies"},"Next evolution(s)":[{"Number":"073","Name":"Tentacruel"}]},{"Number":"073","Name":"Tentacruel","Classification":"Jellyfish Pokemon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Jab"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"072","Name":"Tentacool"}]},{"Number":"074","Name":"Geodude","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"20.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"075","Name":"Graveler"},{"Number":"076","Name":"Golem"}]},{"Number":"075","Name":"Graveler","Classification":"Rock Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"105.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"}],"Next Evolution Requirements":{"Amount":100,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"076","Name":"Golem"}]},{"Number":"076","Name":"Golem","Classification":"Megaton Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"300.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"},{"Number":"075","Name":"Graveler"}]},{"Number":"077","Name":"Ponyta","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Tackle"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ponyta candies"},"Next evolution(s)":[{"Number":"078","Name":"Rapidash"}]},{"Number":"078","Name":"Rapidash","Classification":"Fire Horse Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Low Kick"],"Weight":"95.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"077","Name":"Ponyta"}]},{"Number":"079","Name":"Slowpoke","Classification":"Dopey Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"36.0 kg","Height":"1.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Slowpoke candies"},"Next evolution(s)":[{"Number":"080","Name":"Slowbro"}]},{"Number":"080","Name":"Slowbro","Classification":"Hermit Crab Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"78.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"079","Name":"Slowpoke"}]},{"Number":"081","Name":"Magnemite","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"6.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Magnemite candies"},"Next evolution(s)":[{"Number":"082","Name":"Magneton"}]},{"Number":"082","Name":"Magneton","Classification":"Magnet Pokemon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"60.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"081","Name":"Magnemite"}]},{"Number":"083","Name":"Farfetch'd","Classification":"Wild Duck Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"15.0 kg","Height":"0.8 m"},{"Number":"084","Name":"Doduo","Classification":"Twin Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"39.2 kg","Height":"1.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Doduo candies"},"Next evolution(s)":[{"Number":"085","Name":"Dodrio"}]},{"Number":"085","Name":"Dodrio","Classification":"Triple Bird Pokemon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Feint Attack","Steel Wing"],"Weight":"85.2 kg","Height":"1.8 m","Previous evolution(s)":[{"Number":"084","Name":"Doduo"}]},{"Number":"086","Name":"Seel","Classification":"Sea Lion Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Water Gun"],"Weight":"90.0 kg","Height":"1.1 m","Next Evolution Requirements":{"Amount":50,"Name":"Seel candies"},"Next evolution(s)":[{"Number":"087","Name":"Dewgong"}]},{"Number":"087","Name":"Dewgong","Classification":"Sea Lion Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"120.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"086","Name":"Seel"}]},{"Number":"088","Name":"Grimer","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Mud Slap"],"Weight":"30.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Grimer candies"},"Next evolution(s)":[{"Number":"089","Name":"Muk"}]},{"Number":"089","Name":"Muk","Classification":"Sludge Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Poison Jab",""],"Weight":"30.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"088","Name":"Grimer"}]},{"Number":"090","Name":"Shellder","Classification":"Bivalve Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Tackle"],"Weight":"4.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Shellder candies"},"Next evolution(s)":[{"Number":"091","Name":"Cloyster"}]},{"Number":"091","Name":"Cloyster","Classification":"Bivalve Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"132.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"090","Name":"Shellder"}]},{"Number":"092","Name":"Gastly","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Sucker Punch"],"Weight":"0.1 kg","Height":"1.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"093","Name":"Haunter"},{"Number":"094","Name":"Gengar"}]},{"Number":"093","Name":"Haunter","Classification":"Gas Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Shadow Claw"],"Weight":"0.1 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"}],"Next Evolution Requirements":{"Amount":100,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"094","Name":"Gengar"}]},{"Number":"094","Name":"Gengar","Classification":"Shadow Pokemon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Shadow Claw","Sucker Punch"],"Weight":"40.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"},{"Number":"093","Name":"Haunter"}]},{"Number":"095","Name":"Onix","Classification":"Rock Snake Pokemon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"210.0 kg","Height":"8.8 m"},{"Number":"096","Name":"Drowzee","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Pound"],"Weight":"32.4 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Drowzee candies"},"Next evolution(s)":[{"Number":"097","Name":"Hypno"}]},{"Number":"097","Name":"Hypno","Classification":"Hypnosis Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"75.6 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"096","Name":"Drowzee"}]},{"Number":"098","Name":"Krabby","Classification":"River Crab Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Krabby candies"},"Next evolution(s)":[{"Number":"099","Name":"Kingler"}]},{"Number":"099","Name":"Kingler","Classification":"Pincer Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"098","Name":"Krabby"}]},{"Number":"100","Name":"Voltorb","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark","Tackle"],"Weight":"10.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Voltorb candies"},"Next evolution(s)":[{"Number":"101","Name":"Electrode"}]},{"Number":"101","Name":"Electrode","Classification":"Ball Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark",""],"Weight":"66.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"100","Name":"Voltorb"}]},{"Number":"102","Name":"Exeggcute","Classification":"Egg Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion",""],"Weight":"2.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Exeggcute candies"},"Next evolution(s)":[{"Number":"103","Name":"Exeggutor"}]},{"Number":"103","Name":"Exeggutor","Classification":"Coconut Pokemon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"120.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"102","Name":"Exeggcute"}]},{"Number":"104","Name":"Cubone","Classification":"Lonely Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Cubone candies"},"Next evolution(s)":[{"Number":"105","Name":"Marowak"}]},{"Number":"105","Name":"Marowak","Classification":"Bone Keeper Pokemon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"45.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"104","Name":"Cubone"}]},{"Number":"106","Name":"Hitmonlee","Classification":"Kicking Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Low Kick","Rock Smash"],"Weight":"49.8 kg","Height":"1.5 m","Next evolution(s)":[{"Number":"107","Name":"Hitmonchan"}]},{"Number":"107","Name":"Hitmonchan","Classification":"Punching Pokemon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Rock Smash"],"Weight":"50.2 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"106","Name":"Hitmonlee"}]},{"Number":"108","Name":"Lickitung","Classification":"Licking Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"65.5 kg","Height":"1.2 m"},{"Number":"109","Name":"Koffing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"1.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Koffing candies"},"Next evolution(s)":[{"Number":"110","Name":"Weezing"}]},{"Number":"110","Name":"Weezing","Classification":"Poison Gas Pokemon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"9.5 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"109","Name":"Koffing"}]},{"Number":"111","Name":"Rhyhorn","Classification":"Spikes Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"115.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Rhyhorn candies"},"Next evolution(s)":[{"Number":"112","Name":"Rhydon"}]},{"Number":"112","Name":"Rhydon","Classification":"Drill Pokemon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"120.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"111","Name":"Rhyhorn"}]},{"Number":"113","Name":"Chansey","Classification":"Egg Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"34.6 kg","Height":"1.1 m"},{"Number":"114","Name":"Tangela","Classification":"Vine Pokemon","Type I":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug"],"Fast Attack(s)":["Vine Whip",""],"Weight":"35.0 kg","Height":"1.0 m"},{"Number":"115","Name":"Kangaskhan","Classification":"Parent Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Low Kick",""],"Weight":"80.0 kg","Height":"2.2 m"},{"Number":"116","Name":"Horsea","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Water Gun"],"Weight":"8.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Horsea candies"},"Next evolution(s)":[{"Number":"117","Name":"Seadra"}]},{"Number":"117","Name":"Seadra","Classification":"Dragon Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Dragon Breath","Water Gun"],"Weight":"25.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"116","Name":"Horsea"}]},{"Number":"118","Name":"Goldeen","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Mud Shot"],"Weight":"15.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Goldeen candies"},"Next evolution(s)":[{"Number":"119","Name":"Seaking"}]},{"Number":"119","Name":"Seaking","Classification":"Goldfish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Poison Jab"],"Weight":"39.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"118","Name":"Goldeen"}]},{"Number":"120","Name":"Staryu","Classification":"Starshape Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"34.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Staryu candies"},"Next evolution(s)":[{"Number":"120","Name":"Staryu"}]},{"Number":"121","Name":"Starmie","Classification":"Mysterious Pokemon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"80.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"121","Name":"Starmie"}]},{"Number":"122","Name":"Mr. Mime","Classification":"Barrier Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"54.5 kg","Height":"1.3 m"},{"Number":"123","Name":"Scyther","Classification":"Mantis Pokemon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Steel Wing"],"Weight":"56.0 kg","Height":"1.5 m"},{"Number":"124","Name":"Jynx","Classification":"Humanshape Pokemon","Type I":["Ice"],"Type II":["Psychic"],"Weaknesses":["Fire","Bug","Rock","Ghost","Dark","Steel"],"Fast Attack(s)":["Frost Breath","Pound"],"Weight":"40.6 kg","Height":"1.4 m"},{"Number":"125","Name":"Electabuzz","Classification":"Electric Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Low Kick","Thunder Shock"],"Weight":"30.0 kg","Height":"1.1 m"},{"Number":"126","Name":"Magmar","Classification":"Spitfire Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Karate Chop"],"Weight":"44.5 kg","Height":"1.3 m"},{"Number":"127","Name":"Pinsir","Classification":"Stagbeetle Pokemon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Rock Smash"],"Weight":"55.0 kg","Height":"1.5 m"},{"Number":"128","Name":"Tauros","Classification":"Wild Bull Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Tackle","Zen Headbutt"],"Weight":"88.4 kg","Height":"1.4 m"},{"Number":"129","Name":"Magikarp","Classification":"Fish Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Splash",""],"Weight":"10.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":400,"Name":"Magikarp candies"},"Next evolution(s)":[{"Number":"130","Name":"Gyarados"}]},{"Number":"130","Name":"Gyarados","Classification":"Atrocious Pokemon","Type I":["Water"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Bite","Dragon Breath"],"Weight":"235.0 kg","Height":"6.5 m","Previous evolution(s)":[{"Number":"129","Name":"Magikarp"}]},{"Number":"131","Name":"Lapras","Classification":"Transport Pokemon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"220.0 kg","Height":"2.5 m"},{"Number":"132","Name":"Ditto","Classification":"Transform Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.3 m"},{"Number":"133","Name":"Eevee","Classification":"Evolution Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"6.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Eevee candies"},"Next evolution(s)":[{"Number":"134","Name":"Vaporeon"},{"Number":"135","Name":"Jolteon"},{"Number":"136","Name":"Flareon"}]},{"Number":"134","Name":"Vaporeon","Classification":"Bubble Jet Pokemon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun",""],"Weight":"29.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"135","Name":"Jolteon","Classification":"Lightning Pokemon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock",""],"Weight":"24.5 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"136","Name":"Flareon","Classification":"Flame Pokemon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"25.0 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"137","Name":"Porygon","Classification":"Virtual Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"36.5 kg","Height":"0.8 m"},{"Number":"138","Name":"Omanyte","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Water Gun",""],"Weight":"7.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Omanyte candies"},"Next evolution(s)":[{"Number":"139","Name":"Omastar"}]},{"Number":"139","Name":"Omastar","Classification":"Spiral Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Rock Throw","Water Gun"],"Weight":"35.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"138","Name":"Omanyte"}]},{"Number":"140","Name":"Kabuto","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"11.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Kabuto candies"},"Next evolution(s)":[{"Number":"141","Name":"Kabutops"}]},{"Number":"141","Name":"Kabutops","Classification":"Shellfish Pokemon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Fury Cutter","Mud Shot"],"Weight":"40.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"140","Name":"Kabuto"}]},{"Number":"142","Name":"Aerodactyl","Classification":"Fossil Pokemon","Type I":["Rock"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Ice","Rock","Steel"],"Fast Attack(s)":["Bite","Steel Wing"],"Weight":"59.0 kg","Height":"1.8 m"},{"Number":"143","Name":"Snorlax","Classification":"Sleeping Pokemon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"460.0 kg","Height":"2.1 m"},{"Number":"144","Name":"Articuno","Classification":"Freeze Pokemon","Type I":["Ice"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Rock","Steel"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"55.4 kg","Height":"1.7 m"},{"Number":"145","Name":"Zapdos","Classification":"Electric Pokemon","Type I":["Electric"],"Type II":["Flying"],"Weaknesses":["Ice","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"52.6 kg","Height":"1.6 m"},{"Number":"146","Name":"Moltres","Classification":"Flame Pokemon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"60.0 kg","Height":"2.0 m"},{"Number":"147","Name":"Dratini","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"3.3 kg","Height":"1.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Dratini candies"}},{"Number":"148","Name":"Dragonair","Classification":"Dragon Pokemon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"16.5 kg","Height":"4.0 m","Next Evolution Requirements":{"Amount":100,"Name":"Dratini candies"},"Next evolution(s)":[{"Number":"149","Name":"Dragonite"}]},{"Number":"149","Name":"Dragonite","Classification":"Dragon Pokemon","Type I":["Dragon"],"Type II":["Flying"],"Weaknesses":["Ice","Rock","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath","Steel Wing"],"Weight":"210.0 kg","Height":"2.2 m","Previous evolution(s)":[{"Number":"148","Name":"Dragonair"}]},{"Number":"150","Name":"Mewtwo","Classification":"Genetic Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"122.0 kg","Height":"2.0 m"},{"Number":"151","Name":"Mew","Classification":"New Species Pokemon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.4 m"}] \ No newline at end of file +[ + { + "Number": "001", + "Name": "Bulbasaur", + "Classification": "Seed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Tackle", + "Vine Whip" + ], + "Weight": "6.9 kg", + "Height": "0.7 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 1, + "Name": "Bulbasaur candies" + }, + "Next evolution(s)": [ + { + "Number": "002", + "Name": "Ivysaur" + }, + { + "Number": "003", + "Name": "Venusaur" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Seed Bomb", + "Sludge Bomb" + ], + "BaseAttack": 126, + "BaseDefense": 126, + "BaseStamina": 90, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "002", + "Name": "Ivysaur", + "Classification": "Seed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Razor Leaf", + "Vine Whip" + ], + "Weight": "13.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "001", + "Name": "Bulbasaur" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 1, + "Name": "Bulbasaur candies" + }, + "Next evolution(s)": [ + { + "Number": "003", + "Name": "Venusaur" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 156, + "BaseDefense": 158, + "BaseStamina": 120, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "003", + "Name": "Venusaur", + "Classification": "Seed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Razor Leaf", + "Vine Whip" + ], + "Weight": "100.0 kg", + "Height": "2.0 m", + "Previous evolution(s)": [ + { + "Number": "001", + "Name": "Bulbasaur" + }, + { + "Number": "002", + "Name": "Ivysaur" + } + ], + "Special Attack(s)": [ + "Petal Blizzard", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 198, + "BaseDefense": 200, + "BaseStamina": 160, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "004", + "Name": "Charmander", + "Classification": "Lizard Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Scratch" + ], + "Weight": "8.5 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 4, + "Name": "Charmander candies" + }, + "Next evolution(s)": [ + { + "Number": "005", + "Name": "Charmeleon" + }, + { + "Number": "006", + "Name": "Charizard" + } + ], + "Special Attack(s)": [ + "Flame Burst", + "Flame Charge", + "Flamethrower" + ], + "BaseAttack": 128, + "BaseDefense": 108, + "BaseStamina": 78, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "005", + "Name": "Charmeleon", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Scratch" + ], + "Weight": "19.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "004", + "Name": "Charmander" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 4, + "Name": "Charmander candies" + }, + "Next evolution(s)": [ + { + "Number": "006", + "Name": "Charizard" + } + ], + "Special Attack(s)": [ + "Fire Punch", + "Flame Burst", + "Flamethrower" + ], + "BaseAttack": 160, + "BaseDefense": 140, + "BaseStamina": 116, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "006", + "Name": "Charizard", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Water", + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Wing Attack" + ], + "Weight": "90.5 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "004", + "Name": "Charmander" + }, + { + "Number": "005", + "Name": "Charmeleon" + } + ], + "Special Attack(s)": [ + "Dragon Claw", + "Fire Blast", + "Flamethrower" + ], + "BaseAttack": 212, + "BaseDefense": 182, + "BaseStamina": 156, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "007", + "Name": "Squirtle", + "Classification": "Tiny Turtle Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Tackle" + ], + "Weight": "9.0 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 7, + "Name": "Squirtle candies" + }, + "Next evolution(s)": [ + { + "Number": "008", + "Name": "Wartortle" + }, + { + "Number": "009", + "Name": "Blastoise" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Aqua Tail", + "Water Pulse" + ], + "BaseAttack": 112, + "BaseDefense": 142, + "BaseStamina": 88, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "008", + "Name": "Wartortle", + "Classification": "Turtle Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bite", + "Water Gun" + ], + "Weight": "22.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "007", + "Name": "Squirtle" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 7, + "Name": "Squirtle candies" + }, + "Next evolution(s)": [ + { + "Number": "009", + "Name": "Blastoise" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Hydro Pump", + "Ice Beam" + ], + "BaseAttack": 144, + "BaseDefense": 176, + "BaseStamina": 118, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "009", + "Name": "Blastoise", + "Classification": "Shellfish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bite", + "Water Gun" + ], + "Weight": "85.5 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "007", + "Name": "Squirtle" + }, + { + "Number": "008", + "Name": "Wartortle" + } + ], + "Special Attack(s)": [ + "Flash Cannon", + "Hydro Pump", + "Ice Beam" + ], + "BaseAttack": 186, + "BaseDefense": 222, + "BaseStamina": 158, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "010", + "Name": "Caterpie", + "Classification": "Worm Pokemon", + "Type I": [ + "Bug" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Tackle" + ], + "Weight": "2.9 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 12, + "Family": 10, + "Name": "Caterpie candies" + }, + "Next evolution(s)": [ + { + "Number": "011", + "Name": "Metapod" + }, + { + "Number": "012", + "Name": "Butterfree" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 62, + "BaseDefense": 66, + "BaseStamina": 90, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "011", + "Name": "Metapod", + "Classification": "Cocoon Pokemon", + "Type I": [ + "Bug" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Tackle" + ], + "Weight": "9.9 kg", + "Height": "0.7 m", + "Previous evolution(s)": [ + { + "Number": "010", + "Name": "Caterpie" + } + ], + "Next Evolution Requirements": { + "Amount": 50, + "Family": 10, + "Name": "Caterpie candies" + }, + "Next evolution(s)": [ + { + "Number": "012", + "Name": "Butterfree" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 56, + "BaseDefense": 86, + "BaseStamina": 100, + "CaptureRate": 0.2, + "FleeRate": 0.09 + }, + { + "Number": "012", + "Name": "Butterfree", + "Classification": "Butterfly Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Fire", + "Electric", + "Ice", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Confusion" + ], + "Weight": "32.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "010", + "Name": "Caterpie" + }, + { + "Number": "011", + "Name": "Metapod" + } + ], + "Special Attack(s)": [ + "Bug Buzz", + "Psychic", + "Signal Beam" + ], + "BaseAttack": 144, + "BaseDefense": 144, + "BaseStamina": 120, + "CaptureRate": 0.1, + "FleeRate": 0.06 + }, + { + "Number": "013", + "Name": "Weedle", + "Classification": "Hairy Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Poison Sting" + ], + "Weight": "3.2 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 12, + "Family": 13, + "Name": "Weedle candies" + }, + "Next evolution(s)": [ + { + "Number": "014", + "Name": "Kakuna" + }, + { + "Number": "015", + "Name": "Beedrill" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 68, + "BaseDefense": 64, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "014", + "Name": "Kakuna", + "Classification": "Cocoon Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Poison Sting" + ], + "Weight": "10.0 kg", + "Height": "0.6 m", + "Previous evolution(s)": [ + { + "Number": "013", + "Name": "Weedle" + } + ], + "Next Evolution Requirements": { + "Amount": 50, + "Family": 13, + "Name": "Weedle candies" + }, + "Next evolution(s)": [ + { + "Number": "015", + "Name": "Beedrill" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 62, + "BaseDefense": 82, + "BaseStamina": 90, + "CaptureRate": 0.2, + "FleeRate": 0.09 + }, + { + "Number": "015", + "Name": "Beedrill", + "Classification": "Poison Bee Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Poison Jab" + ], + "Weight": "29.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "013", + "Name": "Weedle" + }, + { + "Number": "014", + "Name": "Kakuna" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Sludge Bomb", + "X Scissor" + ], + "BaseAttack": 144, + "BaseDefense": 130, + "BaseStamina": 130, + "CaptureRate": 0.1, + "FleeRate": 0.06 + }, + { + "Number": "016", + "Name": "Pidgey", + "Classification": "Tiny Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Twister" + ], + "Weight": "1.8 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 12, + "Family": 16, + "Name": "Pidgey candies" + }, + "Next evolution(s)": [ + { + "Number": "017", + "Name": "Pidgeotto" + }, + { + "Number": "018", + "Name": "Pidgeot" + } + ], + "BaseAttack": 94, + "BaseDefense": 90, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "017", + "Name": "Pidgeotto", + "Classification": "Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Steel Wing", + "Wing Attack" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Twister" + ], + "Weight": "30.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "016", + "Name": "Pidgey" + } + ], + "Next Evolution Requirements": { + "Amount": 50, + "Family": 16, + "Name": "Pidgey candies" + }, + "Next evolution(s)": [ + { + "Number": "018", + "Name": "Pidgeot" + } + ], + "BaseAttack": 126, + "BaseDefense": 122, + "BaseStamina": 126, + "CaptureRate": 0.2, + "FleeRate": 0.09 + }, + { + "Number": "018", + "Name": "Pidgeot", + "Classification": "Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Steel Wing", + "Wing Attack" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Hurricane" + ], + "Weight": "39.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "016", + "Name": "Pidgey" + }, + { + "Number": "017", + "Name": "Pidgeotto" + } + ], + "BaseAttack": 170, + "BaseDefense": 166, + "BaseStamina": 166, + "CaptureRate": 0.1, + "FleeRate": 0.06 + }, + { + "Number": "019", + "Name": "Rattata", + "Classification": "Mouse Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Special Attack(s)": [ + "Body Slam", + "Dig", + "Hyper Fang" + ], + "Weight": "3.5 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 19, + "Name": "Rattata candies" + }, + "Next evolution(s)": [ + { + "Number": "020", + "Name": "Raticate" + } + ], + "BaseAttack": 92, + "BaseDefense": 86, + "BaseStamina": 60, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "020", + "Name": "Raticate", + "Classification": "Mouse Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Bite", + "Quick Attack" + ], + "Special Attack(s)": [ + "Dig", + "Hyper Beam", + "Hyper Fang" + ], + "Weight": "18.5 kg", + "Height": "0.7 m", + "Previous evolution(s)": [ + { + "Number": "019", + "Name": "Rattata" + } + ], + "BaseAttack": 146, + "BaseDefense": 150, + "BaseStamina": 110, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "021", + "Name": "Spearow", + "Classification": "Tiny Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Peck", + "Quick Attack" + ], + "Weight": "2.0 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 21, + "Name": "Spearow candies" + }, + "Next evolution(s)": [ + { + "Number": "022", + "Name": "Fearow" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Drill Peck", + "Twister" + ], + "BaseAttack": 102, + "BaseDefense": 78, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "022", + "Name": "Fearow", + "Classification": "Beak Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Peck", + "Steel Wing" + ], + "Weight": "38.0 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "021", + "Name": "Spearow" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Drill Run", + "Twister" + ], + "BaseAttack": 168, + "BaseDefense": 146, + "BaseStamina": 130, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "023", + "Name": "Ekans", + "Classification": "Snake Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Poison Sting" + ], + "Weight": "6.9 kg", + "Height": "2.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 23, + "Name": "Ekans candies" + }, + "Next evolution(s)": [ + { + "Number": "024", + "Name": "Arbok" + } + ], + "Special Attack(s)": [ + "Gunk Shot", + "Sludge Bomb", + "Wrap" + ], + "BaseAttack": 112, + "BaseDefense": 112, + "BaseStamina": 70, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "024", + "Name": "Arbok", + "Classification": "Cobra Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Bite" + ], + "Weight": "65.0 kg", + "Height": "3.5 m", + "Previous evolution(s)": [ + { + "Number": "023", + "Name": "Ekans" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Gunk Shot", + "Sludge Wave" + ], + "BaseAttack": 166, + "BaseDefense": 166, + "BaseStamina": 120, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "025", + "Name": "Pikachu", + "Classification": "Mouse Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Thunder Shock" + ], + "Weight": "6.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 25, + "Name": "Pikachu candies" + }, + "Next evolution(s)": [ + { + "Number": "026", + "Name": "Raichu" + } + ], + "Special Attack(s)": [ + "Discharge", + "Thunder", + "Thunderbolt" + ], + "BaseAttack": 124, + "BaseDefense": 108, + "BaseStamina": 70, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "026", + "Name": "Raichu", + "Classification": "Mouse Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Thunder Shock" + ], + "Weight": "30.0 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "025", + "Name": "Pikachu" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Thunder", + "Thunder Punch" + ], + "BaseAttack": 200, + "BaseDefense": 154, + "BaseStamina": 120, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "027", + "Name": "Sandshrew", + "Classification": "Mouse Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Scratch" + ], + "Weight": "12.0 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 27, + "Name": "Sandshrew candies" + }, + "Next evolution(s)": [ + { + "Number": "028", + "Name": "Sandslash" + } + ], + "Special Attack(s)": [ + "Dig", + "Rock Slide", + "Rock Tomb" + ], + "BaseAttack": 90, + "BaseDefense": 114, + "BaseStamina": 100, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "028", + "Name": "Sandslash", + "Classification": "Mouse Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Metal Claw", + "Mud Shot" + ], + "Weight": "29.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "027", + "Name": "Sandshrew" + } + ], + "Special Attack(s)": [ + "Bulldoze", + "Earthquake", + "Rock Tomb" + ], + "BaseAttack": 150, + "BaseDefense": 172, + "BaseStamina": 150, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "029", + "Name": "Nidoran F", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bite", + "Poison Sting" + ], + "Weight": "7.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 29, + "Name": "Nidoran F candies" + }, + "Next evolution(s)": [ + { + "Number": "030", + "Name": "Nidorina" + }, + { + "Number": "031", + "Name": "Nidoqueen" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Poison Fang", + "Sludge Bomb" + ], + "BaseAttack": 100, + "BaseDefense": 104, + "BaseStamina": 110, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "030", + "Name": "Nidorina", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bite", + "Poison Sting" + ], + "Weight": "20.0 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "029", + "Name": "Nidoran F" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 29, + "Name": "Nidoran F candies" + }, + "Next evolution(s)": [ + { + "Number": "031", + "Name": "Nidoqueen" + } + ], + "Special Attack(s)": [ + "Dig", + "Poison Fang", + "Sludge Bomb" + ], + "BaseAttack": 132, + "BaseDefense": 136, + "BaseStamina": 140, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "031", + "Name": "Nidoqueen", + "Classification": "Drill Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Ice", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bite", + "Poison Jab" + ], + "Weight": "60.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "029", + "Name": "Nidoran F" + }, + { + "Number": "030", + "Name": "Nidorina" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Sludge Wave", + "Stone Edge" + ], + "BaseAttack": 184, + "BaseDefense": 190, + "BaseStamina": 180, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "032", + "Name": "Nidoran M", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Peck", + "Poison Sting" + ], + "Weight": "9.0 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 32, + "Name": "Nidoran M candies" + }, + "Next evolution(s)": [ + { + "Number": "033", + "Name": "Nidorino" + }, + { + "Number": "034", + "Name": "Nidoking" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Horn Attack", + "Sludge Bomb" + ], + "BaseAttack": 110, + "BaseDefense": 94, + "BaseStamina": 92, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "033", + "Name": "Nidorino", + "Classification": "Poison Pin Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Poison Jab", + "Poison Sting" + ], + "Weight": "19.5 kg", + "Height": "0.9 m", + "Previous evolution(s)": [ + { + "Number": "032", + "Name": "Nidoran M" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 32, + "Name": "Nidoran M candies" + }, + "Next evolution(s)": [ + { + "Number": "034", + "Name": "Nidoking" + } + ], + "Special Attack(s)": [ + "Dig", + "Horn Attack", + "Sludge Bomb" + ], + "BaseAttack": 142, + "BaseDefense": 128, + "BaseStamina": 122, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "034", + "Name": "Nidoking", + "Classification": "Drill Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Ice", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Poison Jab" + ], + "Weight": "62.0 kg", + "Height": "1.4 m", + "Previous evolution(s)": [ + { + "Number": "032", + "Name": "Nidoran M" + }, + { + "Number": "033", + "Name": "Nidorino" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Megahorn", + "Sludge Wave" + ], + "BaseAttack": 204, + "BaseDefense": 170, + "BaseStamina": 162, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "035", + "Name": "Clefairy", + "Classification": "Fairy Pokemon", + "Type I": [ + "Fairy" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound", + "Zen Headbutt" + ], + "Weight": "7.5 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 35, + "Name": "Clefairy candies" + }, + "Next evolution(s)": [ + { + "Number": "036", + "Name": "Clefable" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Disarming Voice", + "Moonblast" + ], + "BaseAttack": 116, + "BaseDefense": 124, + "BaseStamina": 140, + "CaptureRate": 0.24, + "FleeRate": 0.1 + }, + { + "Number": "036", + "Name": "Clefable", + "Classification": "Fairy Pokemon", + "Type I": [ + "Fairy" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound", + "Zen Headbutt" + ], + "Weight": "40.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "035", + "Name": "Clefairy" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Moonblast", + "Psychic" + ], + "BaseAttack": 178, + "BaseDefense": 178, + "BaseStamina": 190, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "037", + "Name": "Vulpix", + "Classification": "Fox Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Quick Attack" + ], + "Weight": "9.9 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 37, + "Name": "Vulpix candies" + }, + "Next evolution(s)": [ + { + "Number": "038", + "Name": "Ninetales" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Flame Charge", + "Flamethrower" + ], + "BaseAttack": 106, + "BaseDefense": 118, + "BaseStamina": 76, + "CaptureRate": 0.24, + "FleeRate": 0.1 + }, + { + "Number": "038", + "Name": "Ninetales", + "Classification": "Fox Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Feint Attack" + ], + "Weight": "19.9 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "037", + "Name": "Vulpix" + } + ], + "Special Attack(s)": [ + "Fire Blast", + "Flamethrower", + "Heat Wave" + ], + "BaseAttack": 176, + "BaseDefense": 194, + "BaseStamina": 146, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "039", + "Name": "Jigglypuff", + "Classification": "Balloon Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Fairy" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Pound" + ], + "Weight": "5.5 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 39, + "Name": "Jigglypuff candies" + }, + "Next evolution(s)": [ + { + "Number": "040", + "Name": "Wigglytuff" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Disarming Voice", + "Play Rough" + ], + "BaseAttack": 98, + "BaseDefense": 54, + "BaseStamina": 230, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "040", + "Name": "Wigglytuff", + "Classification": "Balloon Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Fairy" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Pound" + ], + "Weight": "12.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "039", + "Name": "Jigglypuff" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Hyper Beam", + "Play Rough" + ], + "BaseAttack": 168, + "BaseDefense": 108, + "BaseStamina": 280, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "041", + "Name": "Zubat", + "Classification": "Bat Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Ice", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Quick Attack" + ], + "Weight": "7.5 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 41, + "Name": "Zubat candies" + }, + "Next evolution(s)": [ + { + "Number": "042", + "Name": "Golbat" + } + ], + "Special Attack(s)": [ + "Air Cutter", + "Poison Fang", + "Sludge Bomb" + ], + "BaseAttack": 88, + "BaseDefense": 90, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.2 + }, + { + "Number": "042", + "Name": "Golbat", + "Classification": "Bat Pokemon", + "Type I": [ + "Poison" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Ice", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Wing Attack" + ], + "Weight": "55.0 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "041", + "Name": "Zubat" + } + ], + "Special Attack(s)": [ + "Air Cutter", + "Ominous Wind", + "Poison Fang" + ], + "BaseAttack": 164, + "BaseDefense": 164, + "BaseStamina": 150, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "043", + "Name": "Oddish", + "Classification": "Weed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "5.4 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 43, + "Name": "Oddish candies" + }, + "Next evolution(s)": [ + { + "Number": "044", + "Name": "Gloom" + }, + { + "Number": "045", + "Name": "Vileplume" + } + ], + "Special Attack(s)": [ + "Moonblast", + "Seed Bomb", + "Sludge Bomb" + ], + "BaseAttack": 134, + "BaseDefense": 130, + "BaseStamina": 90, + "CaptureRate": 0.48, + "FleeRate": 0.15 + }, + { + "Number": "044", + "Name": "Gloom", + "Classification": "Weed Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "8.6 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "043", + "Name": "Oddish" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 43, + "Name": "Oddish candies" + }, + "Next evolution(s)": [ + { + "Number": "045", + "Name": "Vileplume" + } + ], + "Special Attack(s)": [ + "Moonblast", + "Petal Blizzard", + "Sludge Bomb" + ], + "BaseAttack": 162, + "BaseDefense": 158, + "BaseStamina": 120, + "CaptureRate": 0.24, + "FleeRate": 0.07 + }, + { + "Number": "045", + "Name": "Vileplume", + "Classification": "Flower Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "18.6 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "043", + "Name": "Oddish" + }, + { + "Number": "044", + "Name": "Gloom" + } + ], + "Special Attack(s)": [ + "Moonblast", + "Petal Blizzard", + "Solar Beam" + ], + "BaseAttack": 202, + "BaseDefense": 190, + "BaseStamina": 150, + "CaptureRate": 0.12, + "FleeRate": 0.05 + }, + { + "Number": "046", + "Name": "Paras", + "Classification": "Mushroom Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Grass" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Scratch" + ], + "Weight": "5.4 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 46, + "Name": "Paras candies" + }, + "Next evolution(s)": [ + { + "Number": "047", + "Name": "Parasect" + } + ], + "Special Attack(s)": [ + "Cross Poison", + "Seed Bomb", + "X Scissor" + ], + "BaseAttack": 122, + "BaseDefense": 120, + "BaseStamina": 70, + "CaptureRate": 0.32, + "FleeRate": 0.15 + }, + { + "Number": "047", + "Name": "Parasect", + "Classification": "Mushroom Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Grass" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Fury Cutter" + ], + "Weight": "29.5 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "046", + "Name": "Paras" + } + ], + "Special Attack(s)": [ + "Cross Poison", + "Solar Beam", + "X Scissor" + ], + "BaseAttack": 162, + "BaseDefense": 170, + "BaseStamina": 120, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "048", + "Name": "Venonat", + "Classification": "Insect Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Confusion" + ], + "Weight": "30.0 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 48, + "Name": "Venonat candies" + }, + "Next evolution(s)": [ + { + "Number": "049", + "Name": "Venomoth" + } + ], + "Special Attack(s)": [ + "Poison Fang", + "Psybeam", + "Signal Beam" + ], + "BaseAttack": 108, + "BaseDefense": 118, + "BaseStamina": 120, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "049", + "Name": "Venomoth", + "Classification": "Poison Moth Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Psychic", + "Rock" + ], + "Fast Attack(s)": [ + "Bug Bite", + "Confusion" + ], + "Weight": "12.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "048", + "Name": "Venonat" + } + ], + "Special Attack(s)": [ + "Bug Buzz", + "Poison Fang", + "Psychic" + ], + "BaseAttack": 172, + "BaseDefense": 154, + "BaseStamina": 140, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "050", + "Name": "Diglett", + "Classification": "Mole Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Scratch" + ], + "Weight": "0.8 kg", + "Height": "0.2 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 50, + "Name": "Diglett candies" + }, + "Next evolution(s)": [ + { + "Number": "051", + "Name": "Dugtrio" + } + ], + "Special Attack(s)": [ + "Dig", + "Mud Bomb", + "Rock Tomb" + ], + "BaseAttack": 108, + "BaseDefense": 86, + "BaseStamina": 20, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "051", + "Name": "Dugtrio", + "Classification": "Mole Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Sucker Punch" + ], + "Weight": "33.3 kg", + "Height": "0.7 m", + "Previous evolution(s)": [ + { + "Number": "050", + "Name": "Diglett" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Mud Bomb", + "Stone Edge" + ], + "BaseAttack": 148, + "BaseDefense": 140, + "BaseStamina": 70, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "052", + "Name": "Meowth", + "Classification": "Scratch Cat Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Bite", + "Scratch" + ], + "Weight": "4.2 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 52, + "Name": "Meowth candies" + }, + "Next evolution(s)": [ + { + "Number": "053", + "Name": "Persian" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Dark Pulse", + "Night Slash" + ], + "BaseAttack": 104, + "BaseDefense": 94, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "053", + "Name": "Persian", + "Classification": "Classy Cat Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Scratch" + ], + "Weight": "32.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "052", + "Name": "Meowth" + } + ], + "Special Attack(s)": [ + "Night Slash", + "Play Rough", + "Power Gem" + ], + "BaseAttack": 156, + "BaseDefense": 146, + "BaseStamina": 130, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "054", + "Name": "Psyduck", + "Classification": "Duck Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Water Gun", + "Zen Headbutt" + ], + "Weight": "19.6 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 54, + "Name": "Psyduck candies" + }, + "Next evolution(s)": [ + { + "Number": "055", + "Name": "Golduck" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Cross Chop", + "Psybeam" + ], + "BaseAttack": 132, + "BaseDefense": 112, + "BaseStamina": 100, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "055", + "Name": "Golduck", + "Classification": "Duck Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Confusion", + "Water Gun" + ], + "Weight": "76.6 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "054", + "Name": "Psyduck" + } + ], + "Special Attack(s)": [ + "Hydro Pump", + "Ice Beam", + "Psychic" + ], + "BaseAttack": 194, + "BaseDefense": 176, + "BaseStamina": 160, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "056", + "Name": "Mankey", + "Classification": "Pig Monkey Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Scratch" + ], + "Weight": "28.0 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 56, + "Name": "Mankey candies" + }, + "Next evolution(s)": [ + { + "Number": "057", + "Name": "Primeape" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Cross Chop", + "Low Sweep" + ], + "BaseAttack": 122, + "BaseDefense": 96, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "057", + "Name": "Primeape", + "Classification": "Pig Monkey Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Low Kick" + ], + "Weight": "32.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "056", + "Name": "Mankey" + } + ], + "Special Attack(s)": [ + "Cross Chop", + "Low Sweep", + "Night Slash" + ], + "BaseAttack": 178, + "BaseDefense": 150, + "BaseStamina": 130, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "058", + "Name": "Growlithe", + "Classification": "Puppy Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Ember" + ], + "Weight": "19.0 kg", + "Height": "0.7 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 58, + "Name": "Growlithe candies" + }, + "Next evolution(s)": [ + { + "Number": "059", + "Name": "Arcanine" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Flame Wheel", + "Flamethrower" + ], + "BaseAttack": 156, + "BaseDefense": 110, + "BaseStamina": 110, + "CaptureRate": 0.24, + "FleeRate": 0.1 + }, + { + "Number": "059", + "Name": "Arcanine", + "Classification": "Legendary Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Fire Fang" + ], + "Weight": "155.0 kg", + "Height": "1.9 m", + "Previous evolution(s)": [ + { + "Number": "058", + "Name": "Growlithe" + } + ], + "Special Attack(s)": [ + "Bulldoze", + "Fire Blast", + "Flamethrower" + ], + "BaseAttack": 230, + "BaseDefense": 180, + "BaseStamina": 180, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "060", + "Name": "Poliwag", + "Classification": "Tadpole Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "12.4 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 60, + "Name": "Poliwag candies" + }, + "Next evolution(s)": [ + { + "Number": "061", + "Name": "Poliwhirl" + }, + { + "Number": "062", + "Name": "Poliwrath" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Bubble Beam", + "Mud Bomb" + ], + "BaseAttack": 108, + "BaseDefense": 98, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "061", + "Name": "Poliwhirl", + "Classification": "Tadpole Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "20.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "060", + "Name": "Poliwag" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 60, + "Name": "Poliwag candies" + }, + "Next evolution(s)": [ + { + "Number": "062", + "Name": "Poliwrath" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Mud Bomb", + "Scald" + ], + "BaseAttack": 132, + "BaseDefense": 132, + "BaseStamina": 130, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "062", + "Name": "Poliwrath", + "Classification": "Tadpole Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Fighting" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "54.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "060", + "Name": "Poliwag" + }, + { + "Number": "061", + "Name": "Poliwhirl" + } + ], + "Special Attack(s)": [ + "Hydro Pump", + "Ice Punch", + "Submission" + ], + "BaseAttack": 180, + "BaseDefense": 202, + "BaseStamina": 180, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "063", + "Name": "Abra", + "Classification": "Psi Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Zen Headbutt" + ], + "Weight": "19.5 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 63, + "Name": "Abra candies" + }, + "Next evolution(s)": [ + { + "Number": "064", + "Name": "Kadabra" + }, + { + "Number": "065", + "Name": "Alakazam" + } + ], + "Special Attack(s)": [ + "Psyshock", + "Shadow Ball", + "Signal Beam" + ], + "BaseAttack": 110, + "BaseDefense": 76, + "BaseStamina": 50, + "CaptureRate": 0.4, + "FleeRate": 0.99 + }, + { + "Number": "064", + "Name": "Kadabra", + "Classification": "Psi Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Psycho Cut" + ], + "Weight": "56.5 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "063", + "Name": "Abra" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 63, + "Name": "Abra candies" + }, + "Next evolution(s)": [ + { + "Number": "065", + "Name": "Alakazam" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Psybeam", + "Shadow Ball" + ], + "BaseAttack": 150, + "BaseDefense": 112, + "BaseStamina": 80, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "065", + "Name": "Alakazam", + "Classification": "Psi Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Psycho Cut" + ], + "Weight": "48.0 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "063", + "Name": "Abra" + }, + { + "Number": "064", + "Name": "Kadabra" + } + ], + "Special Attack(s)": [ + "Dazzling Gleam", + "Psychic", + "Shadow Ball" + ], + "BaseAttack": 186, + "BaseDefense": 152, + "BaseStamina": 110, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "066", + "Name": "Machop", + "Classification": "Superpower Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Low Kick" + ], + "Weight": "19.5 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 66, + "Name": "Machop candies" + }, + "Next evolution(s)": [ + { + "Number": "067", + "Name": "Machoke" + }, + { + "Number": "068", + "Name": "Machamp" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Cross Chop", + "Low Sweep" + ], + "BaseAttack": 118, + "BaseDefense": 96, + "BaseStamina": 140, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "067", + "Name": "Machoke", + "Classification": "Superpower Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Karate Chop", + "Low Kick" + ], + "Weight": "70.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "066", + "Name": "Machop" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 66, + "Name": "Machop candies" + }, + "Next evolution(s)": [ + { + "Number": "068", + "Name": "Machamp" + } + ], + "Special Attack(s)": [ + "Brick Break", + "Cross Chop", + "Submission" + ], + "BaseAttack": 154, + "BaseDefense": 144, + "BaseStamina": 160, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "068", + "Name": "Machamp", + "Classification": "Superpower Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Bullet Punch", + "Karate Chop" + ], + "Weight": "130.0 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "066", + "Name": "Machop" + }, + { + "Number": "067", + "Name": "Machoke" + } + ], + "Special Attack(s)": [ + "Cross Chop", + "Stone Edge", + "Submission" + ], + "BaseAttack": 198, + "BaseDefense": 180, + "BaseStamina": 180, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "069", + "Name": "Bellsprout", + "Classification": "Flower Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Vine Whip" + ], + "Weight": "4.0 kg", + "Height": "0.7 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 69, + "Name": "Bellsprout candies" + }, + "Next evolution(s)": [ + { + "Number": "070", + "Name": "Weepinbell" + }, + { + "Number": "071", + "Name": "Victreebel" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Sludge Bomb", + "Wrap" + ], + "BaseAttack": 158, + "BaseDefense": 78, + "BaseStamina": 100, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "070", + "Name": "Weepinbell", + "Classification": "Flycatcher Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "6.4 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "069", + "Name": "Bellsprout" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 69, + "Name": "Bellsprout candies" + }, + "Next evolution(s)": [ + { + "Number": "071", + "Name": "Victreebel" + } + ], + "Special Attack(s)": [ + "Power Whip", + "Seed Bomb", + "Sludge Bomb" + ], + "BaseAttack": 190, + "BaseDefense": 110, + "BaseStamina": 130, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "071", + "Name": "Victreebel", + "Classification": "Flycatcher Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Flying", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Razor Leaf" + ], + "Weight": "15.5 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "069", + "Name": "Bellsprout" + }, + { + "Number": "070", + "Name": "Weepinbell" + } + ], + "Special Attack(s)": [ + "Leaf Blade", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 222, + "BaseDefense": 152, + "BaseStamina": 160, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "072", + "Name": "Tentacool", + "Classification": "Jellyfish Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Electric", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Bubble", + "Poison Sting" + ], + "Weight": "45.5 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 72, + "Name": "Tentacool candies" + }, + "Next evolution(s)": [ + { + "Number": "073", + "Name": "Tentacruel" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Water Pulse", + "Wrap" + ], + "BaseAttack": 106, + "BaseDefense": 136, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "073", + "Name": "Tentacruel", + "Classification": "Jellyfish Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Electric", + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Poison Jab" + ], + "Weight": "55.0 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "072", + "Name": "Tentacool" + } + ], + "Special Attack(s)": [ + "Blizzard", + "Hydro Pump", + "Sludge Wave" + ], + "BaseAttack": 170, + "BaseDefense": 196, + "BaseStamina": 160, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "074", + "Name": "Geodude", + "Classification": "Rock Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Rock Throw", + "Tackle" + ], + "Weight": "20.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 74, + "Name": "Geodude candies" + }, + "Next evolution(s)": [ + { + "Number": "075", + "Name": "Graveler" + }, + { + "Number": "076", + "Name": "Golem" + } + ], + "Special Attack(s)": [ + "Dig", + "Rock Slide", + "Rock Tomb" + ], + "BaseAttack": 106, + "BaseDefense": 118, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "075", + "Name": "Graveler", + "Classification": "Rock Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Rock Throw" + ], + "Weight": "105.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "074", + "Name": "Geodude" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 74, + "Name": "Geodude candies" + }, + "Next evolution(s)": [ + { + "Number": "076", + "Name": "Golem" + } + ], + "Special Attack(s)": [ + "Dig", + "Rock Slide", + "Stone Edge" + ], + "BaseAttack": 142, + "BaseDefense": 156, + "BaseStamina": 110, + "CaptureRate": 0.2, + "FleeRate": 0.07 + }, + { + "Number": "076", + "Name": "Golem", + "Classification": "Megaton Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Rock Throw" + ], + "Weight": "300.0 kg", + "Height": "1.4 m", + "Previous evolution(s)": [ + { + "Number": "074", + "Name": "Geodude" + }, + { + "Number": "075", + "Name": "Graveler" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Earthquake", + "Stone Edge" + ], + "BaseAttack": 176, + "BaseDefense": 198, + "BaseStamina": 160, + "CaptureRate": 0.1, + "FleeRate": 0.05 + }, + { + "Number": "077", + "Name": "Ponyta", + "Classification": "Fire Horse Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Tackle" + ], + "Weight": "30.0 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 77, + "Name": "Ponyta candies" + }, + "Next evolution(s)": [ + { + "Number": "078", + "Name": "Rapidash" + } + ], + "Special Attack(s)": [ + "Fire Blast", + "Flame Charge", + "Flame Wheel" + ], + "BaseAttack": 168, + "BaseDefense": 138, + "BaseStamina": 100, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "078", + "Name": "Rapidash", + "Classification": "Fire Horse Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Low Kick" + ], + "Weight": "95.0 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "077", + "Name": "Ponyta" + } + ], + "Special Attack(s)": [ + "Drill Run", + "Fire Blast", + "Heat Wave" + ], + "BaseAttack": 200, + "BaseDefense": 170, + "BaseStamina": 130, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "079", + "Name": "Slowpoke", + "Classification": "Dopey Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Water Gun" + ], + "Weight": "36.0 kg", + "Height": "1.2 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 79, + "Name": "Slowpoke candies" + }, + "Next evolution(s)": [ + { + "Number": "080", + "Name": "Slowbro" + } + ], + "Special Attack(s)": [ + "Psychic", + "Psyshock", + "Water Pulse" + ], + "BaseAttack": 110, + "BaseDefense": 110, + "BaseStamina": 180, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "080", + "Name": "Slowbro", + "Classification": "Hermit Crab Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Water Gun" + ], + "Weight": "78.5 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "079", + "Name": "Slowpoke" + } + ], + "Special Attack(s)": [ + "Ice Beam", + "Psychic", + "Water Pulse" + ], + "BaseAttack": 184, + "BaseDefense": 198, + "BaseStamina": 190, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "081", + "Name": "Magnemite", + "Classification": "Magnet Pokemon", + "Type I": [ + "Electric" + ], + "Type II": [ + "Steel" + ], + "Weaknesses": [ + "Fire", + "Water", + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Thunder Shock" + ], + "Weight": "6.0 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 81, + "Name": "Magnemite candies" + }, + "Next evolution(s)": [ + { + "Number": "082", + "Name": "Magneton" + } + ], + "Special Attack(s)": [ + "Discharge", + "Magnet Bomb", + "Thunderbolt" + ], + "BaseAttack": 128, + "BaseDefense": 138, + "BaseStamina": 50, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "082", + "Name": "Magneton", + "Classification": "Magnet Pokemon", + "Type I": [ + "Electric" + ], + "Type II": [ + "Steel" + ], + "Weaknesses": [ + "Fire", + "Water", + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Thunder Shock" + ], + "Weight": "60.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "081", + "Name": "Magnemite" + } + ], + "Special Attack(s)": [ + "Discharge", + "Flash Cannon", + "Magnet Bomb" + ], + "BaseAttack": 186, + "BaseDefense": 180, + "BaseStamina": 100, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "083", + "Name": "Farfetch'd", + "Classification": "Wild Duck Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Cut", + "Fury Cutter" + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Leaf Blade" + ], + "Weight": "15.0 kg", + "Height": "0.8 m", + "BaseAttack": 138, + "BaseDefense": 132, + "BaseStamina": 104, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "084", + "Name": "Doduo", + "Classification": "Twin Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Peck", + "Quick Attack" + ], + "Weight": "39.2 kg", + "Height": "1.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 84, + "Name": "Doduo candies" + }, + "Next evolution(s)": [ + { + "Number": "085", + "Name": "Dodrio" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Drill Peck", + "Swift" + ], + "BaseAttack": 126, + "BaseDefense": 96, + "BaseStamina": 70, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "085", + "Name": "Dodrio", + "Classification": "Triple Bird Pokemon", + "Type I": [ + "Normal" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Feint Attack", + "Steel Wing" + ], + "Weight": "85.2 kg", + "Height": "1.8 m", + "Previous evolution(s)": [ + { + "Number": "084", + "Name": "Doduo" + } + ], + "Special Attack(s)": [ + "Aerial Ace", + "Air Cutter", + "Drill Peck" + ], + "BaseAttack": 182, + "BaseDefense": 150, + "BaseStamina": 120, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "086", + "Name": "Seel", + "Classification": "Sea Lion Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Ice Shard", + "Water Gun" + ], + "Weight": "90.0 kg", + "Height": "1.1 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 86, + "Name": "Seel candies" + }, + "Next evolution(s)": [ + { + "Number": "087", + "Name": "Dewgong" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Aqua Tail", + "Icy Wind" + ], + "BaseAttack": 104, + "BaseDefense": 138, + "BaseStamina": 130, + "CaptureRate": 0.4, + "FleeRate": 0.09 + }, + { + "Number": "087", + "Name": "Dewgong", + "Classification": "Sea Lion Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Ice" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Rock" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Ice Shard" + ], + "Weight": "120.0 kg", + "Height": "1.7 m", + "Previous evolution(s)": [ + { + "Number": "086", + "Name": "Seel" + } + ], + "Special Attack(s)": [ + "Aqua Jet", + "Blizzard", + "Icy Wind" + ], + "BaseAttack": 156, + "BaseDefense": 192, + "BaseStamina": 180, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "088", + "Name": "Grimer", + "Classification": "Sludge Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Mud Slap" + ], + "Weight": "30.0 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 88, + "Name": "Grimer candies" + }, + "Next evolution(s)": [ + { + "Number": "089", + "Name": "Muk" + } + ], + "Special Attack(s)": [ + "Mud Bomb", + "Sludge", + "Sludge Bomb" + ], + "BaseAttack": 124, + "BaseDefense": 110, + "BaseStamina": 160, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "089", + "Name": "Muk", + "Classification": "Sludge Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Poison Jab" + ], + "Weight": "30.0 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "088", + "Name": "Grimer" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Gunk Shot", + "Sludge Wave" + ], + "BaseAttack": 180, + "BaseDefense": 188, + "BaseStamina": 210, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "090", + "Name": "Shellder", + "Classification": "Bivalve Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Ice Shard", + "Tackle" + ], + "Weight": "4.0 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 90, + "Name": "Shellder candies" + }, + "Next evolution(s)": [ + { + "Number": "091", + "Name": "Cloyster" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Icy Wind", + "Water Pulse" + ], + "BaseAttack": 120, + "BaseDefense": 112, + "BaseStamina": 60, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "091", + "Name": "Cloyster", + "Classification": "Bivalve Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Ice" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Rock" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Ice Shard" + ], + "Weight": "132.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "090", + "Name": "Shellder" + } + ], + "Special Attack(s)": [ + "Blizzard", + "Hydro Pump", + "Icy Wind" + ], + "BaseAttack": 196, + "BaseDefense": 196, + "BaseStamina": 100, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "092", + "Name": "Gastly", + "Classification": "Gas Pokemon", + "Type I": [ + "Ghost" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Lick", + "Sucker Punch" + ], + "Weight": "0.1 kg", + "Height": "1.3 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 92, + "Name": "Gastly candies" + }, + "Next evolution(s)": [ + { + "Number": "093", + "Name": "Haunter" + }, + { + "Number": "094", + "Name": "Gengar" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Ominous Wind", + "Sludge Bomb" + ], + "BaseAttack": 136, + "BaseDefense": 82, + "BaseStamina": 60, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "093", + "Name": "Haunter", + "Classification": "Gas Pokemon", + "Type I": [ + "Ghost" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Lick", + "Shadow Claw" + ], + "Weight": "0.1 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "092", + "Name": "Gastly" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 92, + "Name": "Gastly candies" + }, + "Next evolution(s)": [ + { + "Number": "094", + "Name": "Gengar" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Shadow Ball", + "Sludge Bomb" + ], + "BaseAttack": 172, + "BaseDefense": 118, + "BaseStamina": 90, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "094", + "Name": "Gengar", + "Classification": "Shadow Pokemon", + "Type I": [ + "Ghost" + ], + "Type II": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Shadow Claw", + "Sucker Punch" + ], + "Weight": "40.5 kg", + "Height": "1.5 m", + "Previous evolution(s)": [ + { + "Number": "092", + "Name": "Gastly" + }, + { + "Number": "093", + "Name": "Haunter" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Shadow Ball", + "Sludge Wave" + ], + "BaseAttack": 204, + "BaseDefense": 156, + "BaseStamina": 120, + "CaptureRate": 0.08, + "FleeRate": 0.05 + }, + { + "Number": "095", + "Name": "Onix", + "Classification": "Rock Snake Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Rock Throw", + "Tackle" + ], + "Weight": "210.0 kg", + "Height": "8.8 m", + "Special Attack(s)": [ + "Iron Head", + "Rock Slide", + "Stone Edge" + ], + "BaseAttack": 90, + "BaseDefense": 186, + "BaseStamina": 70, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "096", + "Name": "Drowzee", + "Classification": "Hypnosis Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Pound" + ], + "Weight": "32.4 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 96, + "Name": "Drowzee candies" + }, + "Next evolution(s)": [ + { + "Number": "097", + "Name": "Hypno" + } + ], + "Special Attack(s)": [ + "Psybeam", + "Psychic", + "Psyshock" + ], + "BaseAttack": 104, + "BaseDefense": 140, + "BaseStamina": 120, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "097", + "Name": "Hypno", + "Classification": "Hypnosis Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Zen Headbutt" + ], + "Weight": "75.6 kg", + "Height": "1.6 m", + "Previous evolution(s)": [ + { + "Number": "096", + "Name": "Drowzee" + } + ], + "Special Attack(s)": [ + "Psychic", + "Psyshock", + "Shadow Ball" + ], + "BaseAttack": 162, + "BaseDefense": 196, + "BaseStamina": 170, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "098", + "Name": "Krabby", + "Classification": "River Crab Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Mud Shot" + ], + "Weight": "6.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 98, + "Name": "Krabby candies" + }, + "Next evolution(s)": [ + { + "Number": "099", + "Name": "Kingler" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Vice Grip", + "Water Pulse" + ], + "BaseAttack": 116, + "BaseDefense": 110, + "BaseStamina": 60, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "099", + "Name": "Kingler", + "Classification": "Pincer Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Metal Claw", + "Mud Shot" + ], + "Weight": "60.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "098", + "Name": "Krabby" + } + ], + "Special Attack(s)": [ + "Vice Grip", + "Water Pulse", + "X Scissor" + ], + "BaseAttack": 178, + "BaseDefense": 168, + "BaseStamina": 110, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "100", + "Name": "Voltorb", + "Classification": "Ball Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Tackle" + ], + "Weight": "10.4 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 100, + "Name": "Voltorb candies" + }, + "Next evolution(s)": [ + { + "Number": "101", + "Name": "Electrode" + } + ], + "Special Attack(s)": [ + "Discharge", + "Signal Beam", + "Thunderbolt" + ], + "BaseAttack": 102, + "BaseDefense": 124, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "101", + "Name": "Electrode", + "Classification": "Ball Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Spark", + "Tackle" + ], + "Weight": "66.6 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "100", + "Name": "Voltorb" + } + ], + "Special Attack(s)": [ + "Discharge", + "Hyper Beam", + "Thunderbolt" + ], + "BaseAttack": 150, + "BaseDefense": 174, + "BaseStamina": 120, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "102", + "Name": "Exeggcute", + "Classification": "Egg Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion" + ], + "Weight": "2.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 102, + "Name": "Exeggcute candies" + }, + "Next evolution(s)": [ + { + "Number": "103", + "Name": "Exeggutor" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Psychic", + "Seed Bomb" + ], + "BaseAttack": 110, + "BaseDefense": 132, + "BaseStamina": 120, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "103", + "Name": "Exeggutor", + "Classification": "Coconut Pokemon", + "Type I": [ + "Grass" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Zen Headbutt" + ], + "Weight": "120.0 kg", + "Height": "2.0 m", + "Previous evolution(s)": [ + { + "Number": "102", + "Name": "Exeggcute" + } + ], + "Special Attack(s)": [ + "Psychic", + "Seed Bomb", + "Solar Beam" + ], + "BaseAttack": 232, + "BaseDefense": 164, + "BaseStamina": 190, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "104", + "Name": "Cubone", + "Classification": "Lonely Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "6.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 104, + "Name": "Cubone candies" + }, + "Next evolution(s)": [ + { + "Number": "105", + "Name": "Marowak" + } + ], + "Special Attack(s)": [ + "Bone Club", + "Bulldoze", + "Dig" + ], + "BaseAttack": 102, + "BaseDefense": 150, + "BaseStamina": 100, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "105", + "Name": "Marowak", + "Classification": "Bone Keeper Pokemon", + "Type I": [ + "Ground" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "45.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "104", + "Name": "Cubone" + } + ], + "Special Attack(s)": [ + "Bone Club", + "Dig", + "Earthquake" + ], + "BaseAttack": 140, + "BaseDefense": 202, + "BaseStamina": 120, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "106", + "Name": "Hitmonlee", + "Classification": "Kicking Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Low Kick", + "Rock Smash" + ], + "Weight": "49.8 kg", + "Height": "1.5 m", + "Special Attack(s)": [ + "Low Sweep", + "Stomp", + "Stone Edge" + ], + "BaseAttack": 148, + "BaseDefense": 172, + "BaseStamina": 100, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "107", + "Name": "Hitmonchan", + "Classification": "Punching Pokemon", + "Type I": [ + "Fighting" + ], + "Weaknesses": [ + "Flying", + "Psychic", + "Fairy" + ], + "Fast Attack(s)": [ + "Bullet Punch", + "Rock Smash" + ], + "Weight": "50.2 kg", + "Height": "1.4 m", + "Special Attack(s)": [ + "Brick Break", + "Fire Punch", + "Ice Punch", + "Thunder Punch" + ], + "BaseAttack": 138, + "BaseDefense": 204, + "BaseStamina": 100, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "108", + "Name": "Lickitung", + "Classification": "Licking Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Lick", + "Zen Headbutt" + ], + "Weight": "65.5 kg", + "Height": "1.2 m", + "Special Attack(s)": [ + "Hyper Beam", + "Power Whip", + "Stomp" + ], + "BaseAttack": 126, + "BaseDefense": 160, + "BaseStamina": 180, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "109", + "Name": "Koffing", + "Classification": "Poison Gas Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Tackle" + ], + "Weight": "1.0 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 109, + "Name": "Koffing candies" + }, + "Next evolution(s)": [ + { + "Number": "110", + "Name": "Weezing" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Sludge", + "Sludge Bomb" + ], + "BaseAttack": 136, + "BaseDefense": 142, + "BaseStamina": 80, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "110", + "Name": "Weezing", + "Classification": "Poison Gas Pokemon", + "Type I": [ + "Poison" + ], + "Weaknesses": [ + "Ground", + "Psychic" + ], + "Fast Attack(s)": [ + "Acid", + "Tackle" + ], + "Weight": "9.5 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "109", + "Name": "Koffing" + } + ], + "Special Attack(s)": [ + "Dark Pulse", + "Shadow Ball", + "Sludge Bomb" + ], + "BaseAttack": 190, + "BaseDefense": 198, + "BaseStamina": 130, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "111", + "Name": "Rhyhorn", + "Classification": "Spikes Pokemon", + "Type I": [ + "Ground" + ], + "Type II": [ + "Rock" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "115.0 kg", + "Height": "1.0 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 111, + "Name": "Rhyhorn candies" + }, + "Next evolution(s)": [ + { + "Number": "112", + "Name": "Rhydon" + } + ], + "Special Attack(s)": [ + "Bulldoze", + "Horn Attack", + "Stomp" + ], + "BaseAttack": 110, + "BaseDefense": 116, + "BaseStamina": 160, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "112", + "Name": "Rhydon", + "Classification": "Drill Pokemon", + "Type I": [ + "Ground" + ], + "Type II": [ + "Rock" + ], + "Weaknesses": [ + "Water", + "Grass", + "Ice", + "Fighting", + "Ground", + "Steel" + ], + "Fast Attack(s)": [ + "Mud Slap", + "Rock Smash" + ], + "Weight": "120.0 kg", + "Height": "1.9 m", + "Previous evolution(s)": [ + { + "Number": "111", + "Name": "Rhyhorn" + } + ], + "Special Attack(s)": [ + "Earthquake", + "Megahorn", + "Stone Edge" + ], + "BaseAttack": 166, + "BaseDefense": 160, + "BaseStamina": 210, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "113", + "Name": "Chansey", + "Classification": "Egg Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound", + "Zen Headbutt" + ], + "Weight": "34.6 kg", + "Height": "1.1 m", + "Special Attack(s)": [ + "Dazzling Gleam", + "Psybeam", + "Psychic" + ], + "BaseAttack": 40, + "BaseDefense": 60, + "BaseStamina": 500, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "114", + "Name": "Tangela", + "Classification": "Vine Pokemon", + "Type I": [ + "Grass" + ], + "Weaknesses": [ + "Fire", + "Ice", + "Poison", + "Flying", + "Bug" + ], + "Fast Attack(s)": [ + "Vine Whip" + ], + "Weight": "35.0 kg", + "Height": "1.0 m", + "Special Attack(s)": [ + "Power Whip", + "Sludge Bomb", + "Solar Beam" + ], + "BaseAttack": 164, + "BaseDefense": 152, + "BaseStamina": 130, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "115", + "Name": "Kangaskhan", + "Classification": "Parent Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Low Kick", + "Mud Slap" + ], + "Weight": "80.0 kg", + "Height": "2.2 m", + "Special Attack(s)": [ + "Brick Break", + "Earthquake", + "Stomp" + ], + "BaseAttack": 142, + "BaseDefense": 178, + "BaseStamina": 210, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "116", + "Name": "Horsea", + "Classification": "Dragon Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Bubble", + "Water Gun" + ], + "Weight": "8.0 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 116, + "Name": "Horsea candies" + }, + "Next evolution(s)": [ + { + "Number": "117", + "Name": "Seadra" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Dragon Pulse", + "Flash Cannon" + ], + "BaseAttack": 122, + "BaseDefense": 100, + "BaseStamina": 60, + "CaptureRate": 0.4, + "FleeRate": 0.1 + }, + { + "Number": "117", + "Name": "Seadra", + "Classification": "Dragon Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Dragon Breath", + "Water Gun" + ], + "Weight": "25.0 kg", + "Height": "1.2 m", + "Previous evolution(s)": [ + { + "Number": "116", + "Name": "Horsea" + } + ], + "Special Attack(s)": [ + "Blizzard", + "Dragon Pulse", + "Hydro Pump" + ], + "BaseAttack": 176, + "BaseDefense": 150, + "BaseStamina": 110, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "118", + "Name": "Goldeen", + "Classification": "Goldfish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Peck" + ], + "Weight": "15.0 kg", + "Height": "0.6 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 118, + "Name": "Goldeen candies" + }, + "Next evolution(s)": [ + { + "Number": "119", + "Name": "Seaking" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Horn Attack", + "Water Pulse" + ], + "BaseAttack": 112, + "BaseDefense": 126, + "BaseStamina": 90, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "119", + "Name": "Seaking", + "Classification": "Goldfish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Peck", + "Poison Jab" + ], + "Weight": "39.0 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "118", + "Name": "Goldeen" + } + ], + "Special Attack(s)": [ + "Drill Run", + "Icy Wind", + "Megahorn" + ], + "BaseAttack": 172, + "BaseDefense": 160, + "BaseStamina": 160, + "CaptureRate": 0.16, + "FleeRate": 0.07 + }, + { + "Number": "120", + "Name": "Staryu", + "Classification": "Starshape Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Water Gun" + ], + "Weight": "34.5 kg", + "Height": "0.8 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 120, + "Name": "Staryu candies" + }, + "Next evolution(s)": [ + { + "Number": "121", + "Name": "Starmie" + } + ], + "Special Attack(s)": [ + "Bubble Beam", + "Power Gem", + "Swift" + ], + "BaseAttack": 130, + "BaseDefense": 128, + "BaseStamina": 60, + "CaptureRate": 0.4, + "FleeRate": 0.15 + }, + { + "Number": "121", + "Name": "Starmie", + "Classification": "Mysterious Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Water Gun" + ], + "Weight": "80.0 kg", + "Height": "1.1 m", + "Previous evolution(s)": [ + { + "Number": "120", + "Name": "Staryu" + } + ], + "Special Attack(s)": [ + "Hydro Pump", + "Power Gem", + "Psybeam" + ], + "BaseAttack": 194, + "BaseDefense": 192, + "BaseStamina": 120, + "CaptureRate": 0.16, + "FleeRate": 0.06 + }, + { + "Number": "122", + "Name": "Mr. Mime", + "Classification": "Barrier Pokemon", + "Type I": [ + "Psychic" + ], + "Type II": [ + "Fairy" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Zen Headbutt" + ], + "Weight": "54.5 kg", + "Height": "1.3 m", + "Special Attack(s)": [ + "Psybeam", + "Psychic", + "Shadow Ball" + ], + "BaseAttack": 154, + "BaseDefense": 196, + "BaseStamina": 80, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "123", + "Name": "Scyther", + "Classification": "Mantis Pokemon", + "Type I": [ + "Bug" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Fire", + "Electric", + "Ice", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Steel Wing" + ], + "Weight": "56.0 kg", + "Height": "1.5 m", + "Special Attack(s)": [ + "Bug Buzz", + "Night Slash", + "X Scissor" + ], + "BaseAttack": 176, + "BaseDefense": 180, + "BaseStamina": 140, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "124", + "Name": "Jynx", + "Classification": "Humanshape Pokemon", + "Type I": [ + "Ice" + ], + "Type II": [ + "Psychic" + ], + "Weaknesses": [ + "Fire", + "Bug", + "Rock", + "Ghost", + "Dark", + "Steel" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Pound" + ], + "Weight": "40.6 kg", + "Height": "1.4 m", + "Special Attack(s)": [ + "Draining Kiss", + "Ice Punch", + "Psyshock" + ], + "BaseAttack": 172, + "BaseDefense": 134, + "BaseStamina": 130, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "125", + "Name": "Electabuzz", + "Classification": "Electric Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Low Kick", + "Thunder Shock" + ], + "Weight": "30.0 kg", + "Height": "1.1 m", + "Special Attack(s)": [ + "Thunder", + "Thunder Punch", + "Thunderbolt" + ], + "BaseAttack": 198, + "BaseDefense": 160, + "BaseStamina": 130, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "126", + "Name": "Magmar", + "Classification": "Spitfire Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember", + "Karate Chop" + ], + "Weight": "44.5 kg", + "Height": "1.3 m", + "Special Attack(s)": [ + "Fire Blast", + "Fire Punch", + "Flamethrower" + ], + "BaseAttack": 214, + "BaseDefense": 158, + "BaseStamina": 130, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "127", + "Name": "Pinsir", + "Classification": "Stagbeetle Pokemon", + "Type I": [ + "Bug" + ], + "Weaknesses": [ + "Fire", + "Flying", + "Rock" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Rock Smash" + ], + "Weight": "55.0 kg", + "Height": "1.5 m", + "Special Attack(s)": [ + "Submission", + "Vice Grip", + "X Scissor" + ], + "BaseAttack": 184, + "BaseDefense": 186, + "BaseStamina": 130, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "128", + "Name": "Tauros", + "Classification": "Wild Bull Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Tackle", + "Zen Headbutt" + ], + "Weight": "88.4 kg", + "Height": "1.4 m", + "Special Attack(s)": [ + "Earthquake", + "Horn Attack", + "Iron Head" + ], + "BaseAttack": 148, + "BaseDefense": 184, + "BaseStamina": 150, + "CaptureRate": 0.24, + "FleeRate": 0.09 + }, + { + "Number": "129", + "Name": "Magikarp", + "Classification": "Fish Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Splash" + ], + "Weight": "10.0 kg", + "Height": "0.9 m", + "Next Evolution Requirements": { + "Amount": 400, + "Family": 129, + "Name": "Magikarp candies" + }, + "Next evolution(s)": [ + { + "Number": "130", + "Name": "Gyarados" + } + ], + "Special Attack(s)": [ + "Struggle" + ], + "BaseAttack": 42, + "BaseDefense": 84, + "BaseStamina": 40, + "CaptureRate": 0.56, + "FleeRate": 0.15 + }, + { + "Number": "130", + "Name": "Gyarados", + "Classification": "Atrocious Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Bite", + "Dragon Breath" + ], + "Weight": "235.0 kg", + "Height": "6.5 m", + "Previous evolution(s)": [ + { + "Number": "129", + "Name": "Magikarp" + } + ], + "Special Attack(s)": [ + "Dragon Pulse", + "Hydro Pump", + "Twister" + ], + "BaseAttack": 192, + "BaseDefense": 196, + "BaseStamina": 190, + "CaptureRate": 0.08, + "FleeRate": 0.07 + }, + { + "Number": "131", + "Name": "Lapras", + "Classification": "Transport Pokemon", + "Type I": [ + "Water" + ], + "Type II": [ + "Ice" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Rock" + ], + "Fast Attack(s)": [ + "Frost Breath", + "Ice Shard" + ], + "Weight": "220.0 kg", + "Height": "2.5 m", + "Special Attack(s)": [ + "Blizzard", + "Dragon Pulse", + "Ice Beam" + ], + "BaseAttack": 186, + "BaseDefense": 190, + "BaseStamina": 260, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "132", + "Name": "Ditto", + "Classification": "Transform Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Pound" + ], + "Special Attack(s)": [ + "Struggle" + ], + "Weight": "4.0 kg", + "Height": "0.3 m", + "BaseAttack": 110, + "BaseDefense": 110, + "BaseStamina": 96, + "CaptureRate": 0.16, + "FleeRate": 0.1 + }, + { + "Number": "133", + "Name": "Eevee", + "Classification": "Evolution Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Weight": "6.5 kg", + "Height": "0.3 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 133, + "Name": "Eevee candies" + }, + "Next evolution(s)": [ + { + "Number": "134", + "Name": "Vaporeon" + }, + { + "Number": "135", + "Name": "Jolteon" + }, + { + "Number": "136", + "Name": "Flareon" + } + ], + "Special Attack(s)": [ + "Body Slam", + "Dig", + "Swift" + ], + "BaseAttack": 114, + "BaseDefense": 128, + "BaseStamina": 110, + "CaptureRate": 0.32, + "FleeRate": 0.1 + }, + { + "Number": "134", + "Name": "Vaporeon", + "Classification": "Bubble Jet Pokemon", + "Type I": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass" + ], + "Fast Attack(s)": [ + "Water Gun" + ], + "Weight": "29.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "133", + "Name": "Eevee" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Hydro Pump", + "Water Pulse" + ], + "BaseAttack": 186, + "BaseDefense": 168, + "BaseStamina": 260, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "135", + "Name": "Jolteon", + "Classification": "Lightning Pokemon", + "Type I": [ + "Electric" + ], + "Weaknesses": [ + "Ground" + ], + "Fast Attack(s)": [ + "Thunder Shock" + ], + "Weight": "24.5 kg", + "Height": "0.8 m", + "Previous evolution(s)": [ + { + "Number": "133", + "Name": "Eevee" + } + ], + "Special Attack(s)": [ + "Discharge", + "Thunder", + "Thunderbolt" + ], + "BaseAttack": 192, + "BaseDefense": 174, + "BaseStamina": 130, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "136", + "Name": "Flareon", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Weaknesses": [ + "Water", + "Ground", + "Rock" + ], + "Fast Attack(s)": [ + "Ember" + ], + "Weight": "25.0 kg", + "Height": "0.9 m", + "Previous evolution(s)": [ + { + "Number": "133", + "Name": "Eevee" + } + ], + "Special Attack(s)": [ + "Fire Blast", + "Flamethrower", + "Heat Wave" + ], + "BaseAttack": 238, + "BaseDefense": 178, + "BaseStamina": 130, + "CaptureRate": 0.12, + "FleeRate": 0.06 + }, + { + "Number": "137", + "Name": "Porygon", + "Classification": "Virtual Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Quick Attack", + "Tackle" + ], + "Weight": "36.5 kg", + "Height": "0.8 m", + "Special Attack(s)": [ + "Discharge", + "Psybeam", + "Signal Beam" + ], + "BaseAttack": 156, + "BaseDefense": 158, + "BaseStamina": 130, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "138", + "Name": "Omanyte", + "Classification": "Spiral Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Water Gun" + ], + "Weight": "7.5 kg", + "Height": "0.4 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 138, + "Name": "Omanyte candies" + }, + "Next evolution(s)": [ + { + "Number": "139", + "Name": "Omastar" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Brine", + "Rock Tomb" + ], + "BaseAttack": 132, + "BaseDefense": 160, + "BaseStamina": 70, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "139", + "Name": "Omastar", + "Classification": "Spiral Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Rock Throw", + "Water Gun" + ], + "Weight": "35.0 kg", + "Height": "1.0 m", + "Previous evolution(s)": [ + { + "Number": "138", + "Name": "Omanyte" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Hydro Pump", + "Rock Slide" + ], + "BaseAttack": 180, + "BaseDefense": 202, + "BaseStamina": 140, + "CaptureRate": 0.12, + "FleeRate": 0.05 + }, + { + "Number": "140", + "Name": "Kabuto", + "Classification": "Shellfish Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Mud Shot", + "Scratch" + ], + "Weight": "11.5 kg", + "Height": "0.5 m", + "Next Evolution Requirements": { + "Amount": 50, + "Family": 140, + "Name": "Kabuto candies" + }, + "Next evolution(s)": [ + { + "Number": "141", + "Name": "Kabutops" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Aqua Jet", + "Rock Tomb" + ], + "BaseAttack": 148, + "BaseDefense": 142, + "BaseStamina": 60, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "141", + "Name": "Kabutops", + "Classification": "Shellfish Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Water" + ], + "Weaknesses": [ + "Electric", + "Grass", + "Fighting", + "Ground" + ], + "Fast Attack(s)": [ + "Fury Cutter", + "Mud Shot" + ], + "Weight": "40.5 kg", + "Height": "1.3 m", + "Previous evolution(s)": [ + { + "Number": "140", + "Name": "Kabuto" + } + ], + "Special Attack(s)": [ + "Ancient Power", + "Stone Edge", + "Water Pulse" + ], + "BaseAttack": 190, + "BaseDefense": 190, + "BaseStamina": 120, + "CaptureRate": 0.12, + "FleeRate": 0.05 + }, + { + "Number": "142", + "Name": "Aerodactyl", + "Classification": "Fossil Pokemon", + "Type I": [ + "Rock" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Water", + "Electric", + "Ice", + "Rock", + "Steel" + ], + "Fast Attack(s)": [ + "Bite", + "Steel Wing" + ], + "Weight": "59.0 kg", + "Height": "1.8 m", + "Special Attack(s)": [ + "Ancient Power", + "Hyper Beam", + "Iron Head" + ], + "BaseAttack": 182, + "BaseDefense": 162, + "BaseStamina": 160, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "143", + "Name": "Snorlax", + "Classification": "Sleeping Pokemon", + "Type I": [ + "Normal" + ], + "Weaknesses": [ + "Fighting" + ], + "Fast Attack(s)": [ + "Lick", + "Zen Headbutt" + ], + "Weight": "460.0 kg", + "Height": "2.1 m", + "Special Attack(s)": [ + "Body Slam", + "Earthquake", + "Hyper Beam" + ], + "BaseAttack": 180, + "BaseDefense": 180, + "BaseStamina": 320, + "CaptureRate": 0.16, + "FleeRate": 0.09 + }, + { + "Number": "144", + "Name": "Articuno", + "Classification": "Freeze Pokemon", + "Type I": [ + "Ice" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Fire", + "Electric", + "Rock", + "Steel" + ], + "Fast Attack(s)": [ + "Frost Breath" + ], + "Special Attack(s)": [ + "Blizzard", + "Ice Beam", + "Icy Wind" + ], + "Weight": "55.4 kg", + "Height": "1.7 m", + "BaseAttack": 198, + "BaseDefense": 242, + "BaseStamina": 180, + "CaptureRate": 0, + "FleeRate": 0.1 + }, + { + "Number": "145", + "Name": "Zapdos", + "Classification": "Electric Pokemon", + "Type I": [ + "Electric" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Ice", + "Rock" + ], + "Fast Attack(s)": [ + "Thunder Shock" + ], + "Special Attack(s)": [ + "Discharge", + "Thunder", + "Thunderbolt" + ], + "Weight": "52.6 kg", + "Height": "1.6 m", + "BaseAttack": 232, + "BaseDefense": 194, + "BaseStamina": 180, + "CaptureRate": 0, + "FleeRate": 0.1 + }, + { + "Number": "146", + "Name": "Moltres", + "Classification": "Flame Pokemon", + "Type I": [ + "Fire" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Water", + "Electric", + "Rock" + ], + "Fast Attack(s)": [ + "Ember" + ], + "Special Attack(s)": [ + "Fire Blast", + "Flamethrower", + "Heat Wave" + ], + "Weight": "60.0 kg", + "Height": "2.0 m", + "BaseAttack": 242, + "BaseDefense": 194, + "BaseStamina": 180, + "CaptureRate": 0, + "FleeRate": 0.1 + }, + { + "Number": "147", + "Name": "Dratini", + "Classification": "Dragon Pokemon", + "Type I": [ + "Dragon" + ], + "Weaknesses": [ + "Ice", + "Dragon", + "Fairy" + ], + "Fast Attack(s)": [ + "Dragon Breath" + ], + "Weight": "3.3 kg", + "Height": "1.8 m", + "Next Evolution Requirements": { + "Amount": 25, + "Family": 147, + "Name": "Dratini candies" + }, + "Next evolution(s)": [ + { + "Number": "148", + "Name": "Dragonair" + }, + { + "Number": "149", + "Name": "Dragonite" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Twister", + "Wrap" + ], + "BaseAttack": 128, + "BaseDefense": 110, + "BaseStamina": 82, + "CaptureRate": 0.32, + "FleeRate": 0.09 + }, + { + "Number": "148", + "Name": "Dragonair", + "Classification": "Dragon Pokemon", + "Type I": [ + "Dragon" + ], + "Weaknesses": [ + "Ice", + "Dragon", + "Fairy" + ], + "Fast Attack(s)": [ + "Dragon Breath" + ], + "Weight": "16.5 kg", + "Height": "4.0 m", + "Previous evolution(s)": [ + { + "Number": "147", + "Name": "Dratini" + } + ], + "Next Evolution Requirements": { + "Amount": 100, + "Family": 147, + "Name": "Dratini candies" + }, + "Next evolution(s)": [ + { + "Number": "149", + "Name": "Dragonite" + } + ], + "Special Attack(s)": [ + "Aqua Tail", + "Dragon Pulse", + "Wrap" + ], + "BaseAttack": 170, + "BaseDefense": 152, + "BaseStamina": 122, + "CaptureRate": 0.08, + "FleeRate": 0.06 + }, + { + "Number": "149", + "Name": "Dragonite", + "Classification": "Dragon Pokemon", + "Type I": [ + "Dragon" + ], + "Type II": [ + "Flying" + ], + "Weaknesses": [ + "Ice", + "Rock", + "Dragon", + "Fairy" + ], + "Fast Attack(s)": [ + "Dragon Breath", + "Steel Wing" + ], + "Weight": "210.0 kg", + "Height": "2.2 m", + "Previous evolution(s)": [ + { + "Number": "147", + "Name": "Dratini" + }, + { + "Number": "148", + "Name": "Dragonair" + } + ], + "Special Attack(s)": [ + "Dragon Claw", + "Dragon Pulse", + "Hyper Beam" + ], + "BaseAttack": 250, + "BaseDefense": 212, + "BaseStamina": 182, + "CaptureRate": 0.04, + "FleeRate": 0.05 + }, + { + "Number": "150", + "Name": "Mewtwo", + "Classification": "Genetic Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Confusion", + "Psycho Cut" + ], + "Special Attack(s)": [ + "Hyper Beam", + "Psychic", + "Shadow Ball" + ], + "Weight": "122.0 kg", + "Height": "2.0 m", + "BaseAttack": 284, + "BaseDefense": 202, + "BaseStamina": 212, + "CaptureRate": 0, + "FleeRate": 0.1 + }, + { + "Number": "151", + "Name": "Mew", + "Classification": "New Species Pokemon", + "Type I": [ + "Psychic" + ], + "Weaknesses": [ + "Bug", + "Ghost", + "Dark" + ], + "Fast Attack(s)": [ + "Pound" + ], + "Special Attack(s)": [ + "Dragon Pulse", + "Earthquake", + "Fire Blast", + "Hurricane", + "Hyper Beam", + "Moonblast", + "Psychic", + "Solar Beam", + "Thunder" + ], + "Weight": "4.0 kg", + "Height": "0.4 m", + "BaseAttack": 220, + "BaseDefense": 220, + "BaseStamina": 200, + "CaptureRate": 0, + "FleeRate": 0.1 + } +] diff --git a/docs/auto_restart.md b/docs/auto_restart.md new file mode 100644 index 0000000000..e85752018c --- /dev/null +++ b/docs/auto_restart.md @@ -0,0 +1,73 @@ +This page is for a workaround to restart your bot(s). +_(Restarting is superior over reconnecting in case of stability for crashes)_ + +# MAC OS +1. Open your terminal +Just open it and you finished step 1 + +2. Create a new apple script +Heres an example to start and restart bots (in separate folders) adjust it for your needs. (paths, start commands, restart timer, ...) + +You can create a start file (if you are lazy :P) and a restart file or just one for both needs + +Start script: + + tell application "Terminal" + activate + do script "cd desktop" in selected tab of the front window #edit your path + do script "cd bots" in selected tab of the front window #edit your path + do script "cd bot1" in selected tab of the front window #edit your path + do script "python pokecli.py" in selected tab of the front window #start with your parameters + + #add more bots + delay 10 + tell application "System Events" + keystroke "t" using {command down} #open a new tab for next bot + end tell + delay 5 + do script "cd .." in selected tab of the front window + do script "cd bot2" in selected tab of the front window + do script "python pokecli.py" in selected tab of the front window + #copy this part for the amount you need + end tell + +restart: + + repeat + + delay 1200 #timer in seconds + tell application "Terminal" + activate + + tell application "System Events" + keystroke "c" using {control down} #close the bot + end tell + delay 3 + do script "clear" in selected tab of the front window #not needed just for nice view + delay 3 + do script "python pokecli.py" in selected tab of the front window #restart with parameters + + #copy for the amount of bots + delay 10 + tell application "System Events" + keystroke "ö" using {command down} #going to the previous tab + end tell + delay 3 + + tell application "System Events" + keystroke "c" using {control down} + end tell + delay 3 + do script "clear" in selected tab of the front window + delay 3 + do script "python pokecli.py" in selected tab of the front window + + #copy for the amount of bots + tell application "System Events" + keystroke "ä" using {command down} #moving the the last tab + end tell + delay 3 + + end tell + + end repeat diff --git a/docs/configuration_files.md b/docs/configuration_files.md new file mode 100644 index 0000000000..a881740b9a --- /dev/null +++ b/docs/configuration_files.md @@ -0,0 +1,271 @@ +## Usage (up-to-date) + 1. copy `config.json.example` to `config.json`. + 2. Edit `config.json` and replace `auth_service`, `username`, `password`, `location` and `gmapkey` with your parameters (other keys are optional, check `Advance Configuration` below) + 3. Simply launch the script with : `./run.sh` or `./pokecli.py` or `python pokecli.py -cf ./configs/config.json` if you want to specify a config file + +## Advanced Configuration +| Parameter | Default | Description | +|------------------|-------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `tasks` | [] | The behaviors you want the bot to do. Read [how to configure tasks](#configuring-tasks). +| `max_steps` | 5 | The steps around your initial location (DEFAULT 5 mean 25 cells around your location) that will be explored +| `forts.avoid_circles` | False | Set whether the bot should avoid circles | +| `forts.max_circle_size` | 10 | How many forts to keep in ignore list | +| `walk` | 4.16 | Set the walking speed in kilometers per hour. (14 km/h is the maximum speed for egg hatching) | +| `action_wait_min` | 1 | Set the minimum time setting for anti-ban time randomizer +| `action_wait_max` | 4 | Set the maximum time setting for anti-ban time randomizer +| `debug` | false | Let the default value here except if you are developer | +| `test` | false | Let the default value here except if you are developer | | +| `location_cache` | true | Bot will start at last known location if you do not have location set in the config | +| `distance_unit` | km | Set the unit to display distance in (km for kilometers, mi for miles, ft for feet) | +| `evolve_cp_min` | 300 | Min. CP for evolve_all function + +## Configuring Tasks +The behaviors of the bot are configured via the `tasks` key in the `config.json`. This enables you to list what you want the bot to do and change the priority of those tasks by reordering them in the list. This list of tasks is run repeatedly and in order. For more information on why we are moving config to this format, check out the [original proposal](https://github.com/PokemonGoF/PokemonGo-Bot/issues/142). + +### Task Options: +* CatchLuredPokemon +* CatchVisiblePokemon +* EvolvePokemon + * `evolve_all`: Default `NONE` | Set to `"all"` to evolve Pokémon if possible when the bot starts. Can also be set to individual Pokémon as well as multiple separated by a comma. e.g "Pidgey,Rattata,Weedle,Zubat" + * `evolve_speed`: Default `20` + * `use_lucky_egg`: Default: `False` +* FollowPath + * `path_mode`: Default `loop` | Set the mode for the path navigator (loop or linear). + * `path_file`: Default `NONE` | Set the file containing the waypoints for the path navigator. +* FollowSpiral +* HandleSoftBan +* IncubateEggs + * `longer_eggs_first`: Default `True` +* MoveToFort +* [MoveToMapPokemon](#sniping-movetolocation) +* NicknamePokemon + * `nickname_template`: Default `""` | See the [Pokemon Nicknaming](#pokemon-nicknaming) section for more details +* RecycleItems + * `item_filter`: Pass a list of unwanted [items (using their JSON codes)](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Item-ID's) to recycle when collected at a Pokestop +* SpinFort +* TransferPokemon + +### Example configuration: +The following configuration tells the bot to transfer all the Pokemon that match the transfer configuration rules, then recycle the items that match its configuration, then catch the pokemon that it can, so on, so forth. Note the last two tasks, MoveToFort and FollowSpiral. When a task is still in progress, it won't run the next things in the list. So it will move towards the fort, on each step running through the list of tasks again. Only when it arrives at the fort and there are no other stops available for it to move towards will it continue to the next step and follow the spiral. + +``` +{ + // ... + "tasks": [ + { + "type": "TransferPokemon" + }, + { + "type": "RecycleItems" + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort" + }, + { + "type": "MoveToFort" + }, + { + "type": "FollowSpiral" + } + ] + // ... +} +``` + +### Specifying configuration for tasks +If you want to configure a given task, you can pass values like this: + +``` +{ + // ... + "tasks": [ + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + } + ] + // ... +} +``` + +### An example task configuration if you only wanted to collect items from forts: +``` +{ + // ... + "tasks": [ + { + "type": "RecycleItems" + }, + { + "type": "SpinFortWorker" + }, + { + "type": "MoveToFortWorker" + } + ], + // ... +} +``` + +## Catch Configuration +Default configuration will capture all Pokémon. + +```"any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}``` + +You can override the global configuration with Pokémon-specific options, such as: + +```"Pidgey": {"catch_above_cp": 0, "catch_above_iv": 0.8", "logic": "and"}``` to only capture Pidgey with a good roll. + +Additionally, you can specify always_capture and never_capture flags. + +For example: ```"Pidgey": {"never_capture": true}``` will stop catching Pidgey entirely. + +## Release Configuration + +### Common configuration + +Default configuration will not release any Pokémon. + +```"release": {"any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}}``` + +You can override the global configuration with Pokémon-specific options, such as: + +```"release": {"Pidgey": {"release_below_cp": 0, "release_below_iv": 0.8, "logic": "or"}}``` to only release Pidgey with bad rolls. + +Additionally, you can specify always_release and never_release flags. For example: + +```"release": {"Pidgey": {"always_release": true}}``` will release all Pidgey caught. + +### Keep the strongest pokemon configuration (dev branch) + +You can set ```"release": {"Pidgey": {"keep_best_cp": 1}}``` or ```"release": {"any": {"keep_best_iv": 1}}```. + +In that case after each capture bot will check that do you have a new Pokémon or not. + +If you don't have it, it will keep it (no matter was it strong or weak Pokémon). + +If you already have it, it will keep a stronger version and will transfer the a weaker one. + +```"release": {"any": {"keep_best_cp": 2}}```, ```"release": {"any": {"keep_best_cp": 10}}``` - can be any number. + +## Evolve All Configuration + +By setting the `evolve_all` attribute in config.json, you can instruct the bot to automatically +evolve specified Pokémon on startup. This is especially useful for batch-evolving after popping up +a lucky egg (currently this needs to be done manually). + +The evolve all mechanism evolves only higher IV/CP Pokémon. It works by sorting the high CP Pokémon (default: 300 CP or higher) +based on their IV values. After evolving all high CP Pokémon, the mechanism will move on to evolving lower CP Pokémon +only based on their CP (if it can). +It will also automatically transfer the evolved Pokémon based on the release configuration. + +Examples on how to use (set in config.json): + +1. "evolve_all": "all" + Will evolve ALL Pokémon. + +2. "evolve_all": "Pidgey,Weedle" + Will only evolve Pidgey and Weedle. + +3. Not setting evolve_all or having any other string would not evolve any Pokémon on startup. + +If you wish to change the default threshold of 300 CP, simply add the following to the config file: + +``` +"evolve_cp_min": +``` + +## Path Navigator Configuration + +Setting the `navigator.type` setting to `path` allows you to specify waypoints which the bot will follow. The waypoints can be loaded from a GPX or JSON file. By default the bot will walk along all specified waypoints and then move directly to the first waypoint again. When setting `navigator.path_mode` to `linear`, the bot will turn around at the last waypoint and along the given waypoints in reverse order. + +An example for a JSON file can be found in `configs/path.example.json`. GPX files can be exported from many online tools, such as gpsies.com.The bot loads the first segment of the first track. + +## Pokemon Nicknaming + +A `nickname_template` can be specified for the `NicknamePokemon` task to allow a nickname template to be applied to all pokemon in the user's inventory. For example, a user wanting all their pokemon to have their IV values as their nickname could use a template `{iv_ads}`, which will cause their pokemon to be named something like `13/7/12` (depending on the pokemon's actual IVs). + +The `NicknamePokemon` task will rename all pokemon in inventory on startup to match the given template and will rename any newly caught/hatched/evolved pokemon as the bot runs. _It may take one or two "ticks" after catching/hatching/evolving a pokemon for it to be renamed. This is intended behavior._ + +> **NOTE:** If you experience frequent `Pokemon not found` error messages, this is because the inventory cache has not been updated after a pokemon was released. This can be remedied by placing the `NicknamePokemon` task above the `TransferPokemon` task in your `config.json` file. + +Niantic imposes a 12-character limit on all pokemon nicknames, so any new nickname will be truncated to 12 characters if over that limit. Thus, it is up to the user to exercise judgment on what template will best suit their need with this constraint in mind. + +Because some pokemon have very long names, you can use the [Format String syntax](https://docs.python.org/2.7/library/string.html#formatstrings) to ensure that your names do not cause your templates to truncate. For example, using `{name:.8s}` causes the Pokemon name to never take up more than 8 characters in the nickname. This would help guarantee that a template like `{name:.8s}_{iv_pct}` never goes over the 12-character limit. + +Valid names in templates are: +- `name` = pokemon name +- `id` = pokemon type id (e.g. 1 for Bulbasaurs) +- `cp` = pokemon's CP +- `iv_attack` = pokemon's attack IV +- `iv_defense` = pokemon's defense IV +- `iv_stamina` = pokemon's stamina IV +- `iv_ads` = pokemon's IVs in `(attack)/(defense)/(stamina)` format (matches web UI format -- A/D/S) +- `iv_sum` = pokemon's IVs as a sum (e.g. 45 when 3 perfect 15 IVs) +- `iv_pct` = pokemon's IVs as a percentage (0-100) + +> **NOTE:** Use a blank template (`""`) to revert all pokemon to their original names (as if they had no nickname). + +Sample usages: +- `"{name}_{iv_pct}"` => `Mankey_69` +- `"{iv_pct}_{iv_ads}"` => `91_15/11/15` +- `""` -> `Mankey` +![sample](https://cloud.githubusercontent.com/assets/8896778/17285954/0fa44a88-577b-11e6-8204-b1302f4294bd.png) + +## Sniping _(MoveToLocation)_ +### Description +This task will fetch current pokemon spawns from /raw_data of an PokemonGo-Map instance. For information on how to properly setup PokemonGo-Map have a look at the Github page of the project [here](https://github.com/AHAAAAAAA/PokemonGo-Map/). There is an example config in `config/config.json.map.example` + +### Options +* `Address` - Address of the webserver of PokemonGo-Map. ex: `http://localhost:5000` +* `Mode` - Which mode to run snipin on + - `distance` - Will move to the nearest pokemon + - `priority` - Will move to the pokemon with the highest priority assigned (tie breaking by distance) +* `prioritize_vips` - Will prioritize vips in distance and priority mode above all normal pokemon if set to true +* `min_time` - Minimum time the pokemon has to be available before despawn +* `max_distance` - Maximum distance the pokemon is allowed to be when walking, ignored when sniping +* `snipe`: + - `True` - Will teleport to target pokemon, encounter it, teleport back then catch it + - `False` - Will walk normally to the pokemon +* `update_map` - disable/enable if the map location should be automatically updated to the bots current location +* `catch` - A dictionary of pokemon to catch with an assigned priority (higher => better) +* `snipe_high_prio_only` - Whether to snipe pokemon above a certain threshold. +* `snipe_high_prio_threshold` - The threshold number corresponding with the `catch` dictionary. Any pokemon above this threshold will be caught. Other will be igonored. + +#### Example +``` +{ + \\ ... + { + "type": "MoveToMapPokemon", + "config": { + "address": "http://localhost:5000", + "max_distance": 500, + "min_time": 60, + "min_ball": 50, + "prioritize_vips": true, + "snipe": true, + "snipe_high_prio_only": true, + "snipe_high_prio_threshold": 400, + "update_map": true, + "mode": "priority", + "catch": { + "Aerodactyl": 1000, + "Ditto": 900, + "Omastar": 500, + "Omanyte": 150, + "Caterpie": 10, + } + } + } + \\ ... +} +``` diff --git a/docs/develop.md b/docs/develop.md new file mode 100644 index 0000000000..7a1649219f --- /dev/null +++ b/docs/develop.md @@ -0,0 +1,28 @@ +> $ git clone --recursive -b dev https://github.com/PokemonGoF/PokemonGo-Bot +> $ cd PokemonGo-Bot +> // create virtualenv using Python 2.7 executable +> $ virtualenv -p C:\python27\python.exe venv +> $ source venv/Scripts/activate +> $ pip install -r requirements.txt + +Once you are you to date with [dev-branch] (https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) create a pull request and it will be re-viewed + + +### How to add/discover new API +The example is [here](https://github.com/PokemonGoF/PokemonGo-Bot/commit/46e2352ce9f349cc127a408959679282f9999585) +1. Check the type of your API request in [POGOProtos](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/RequestType.proto) For example: RECYCLE_INVENTORY_ITEM +2. Convert to the api call in pokemongo_bot/__init__.py, RECYCLE_INVENTORY_ITEM change to self.api.recycle_inventory_item +``` +def drop_item(self,item_id,count): + self.api.recycle_inventory_item(...............) +``` +3. Where is the param list? +You need check this [Requests/Messages/RecycleInventoryItemMessage.proto](https://github.com/AeonLucid/POGOProtos/blob/eeccbb121b126aa51fc4eebae8d2f23d013e1cb8/src/POGOProtos/Networking/Requests/Messages/RecycleInventoryItemMessage.proto) +4. Then our final api call is +``` +def drop_item(self,item_id,count): + self.api.recycle_inventory_item(item_id=item_id,count=count) + inventory_req = self.api.call() + print(inventory_req) +``` +5. You can now debug on the log to see if get what you need diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000000..04823a2f5c --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,25 @@ +Start by downloading for your platform: [Mac](https://www.docker.com/products/docker#/mac), [Windows](https://www.docker.com/products/docker#/windows), or [Linux](https://www.docker.com/products/docker#/linux). Once you have Docker installed, simply create the various config.json files for your different accounts (e.g. `configs/config-account1.json`) and then create a Docker image for PokemonGo-Bot using the Dockerfile in this repo. +``` +cd PokemonGo-Bot +docker build -t pokemongo-bot . +``` +You can verify that the image was created with: +``` +docker images +``` + +To run PokemonGo-Bot Docker image you've created, simple run: +``` +docker run --name=pokego-bot1 --rm -it -v $(pwd)/configs/config-account1.json:/usr/src/app/configs/config.json pokemongo-bot +``` +_Check the logs in real-time `docker logs -f pgobot`_ + +If you want to run multiple accounts with the same Docker image, simply specify different config.json and names in the Docker run command. +Do not push your image to a registry with your config.json and account details in it! + +Share web folder with host: +``` +docker run -it -v $(pwd)/web/:/usr/src/app/web --rm --name=pgo-bot-acct1 pokemongo-bot --config config.json +``` + +TODO: Add configuration for running multiple Docker containers from the same image for every bot instance, and a single container for the web UI. diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000000..7720f0a64c --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,49 @@ +### How do I start the application? +After customizing your config.json files, cd to the PokemonGo-Bot folder and enter: +``` +$ python pokecli.py +``` +This will start the application. + +### Python possible bug +If you encounter problems with the module `ssl` and it's function `_create_unverified_context`, just comment it. (Solution available in Python 2.7.11) +In order to comment out the function and the module, please follow the instructions below: +- edit `pokecli.py` +- put `#` before `if` (line 43) and `ssl` (line 44) +- save it + +Please keep in mind that this fix is only necessary if your python version don't have the `_create_unverified_context` argument in the ssl module. + +### What's IV? +Here's the [introduction](http://bulbapedia.bulbagarden.net/wiki/Individual_values) + +### Does it run automatically? +Not yet, still need a trainer to train the script param. But we are very close to. + +### Set GEO Location +It works, use "location": "59.333409,18.045008", in configs/config.json to set lat long for location. Use a Pokemon Go map to find an area with pokemons you still need (e.g. [https://pokevision.com/](https://pokevision.com/)), however don't jump too big distances (see "softban"). + +### Google login issues (Login Error, Server busy)? +Try to generate an [app password](!https://support.google.com/accounts/answer/185833?hl=en) and set is as +``` +-p "" +``` +This error mostly occurs for those who are using 2 factor authentication, but either way, for the purpose of security it would be nice to have a separate password for the bot app. + +### FLEE +The status code "3" corresponds to "Flee" - meaning your Pokemon has ran away. + {"responses": { "CATCH_POKEMON": { "status": 3 } } + +### My pokemon are not showing up in my Pokedex? +Finish the tutorial on a smartphone. This will then allow everything to be visible. + +### How can I maximise my XP per hour? +Quick Tip: When using this script, use a Lucky egg to double the XP for 30 mins. You will level up much faster. A Lucky egg is obtained on level 9 and further on whilst leveling up. (from VipsForever via /r/pokemongodev) + +### How do I use the map?? +[See wiki info here] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page)) + +### No JSON object could be decoded or decoder.py error +If you see "No JSON object could be decoded" or you see "decoder.py" in the last part of the error, this means that there is something wrong with your JSON. + +Copy the json in json files and copy it into http://jsonlint.com/ Then fix the error it gives you in your json. diff --git a/docs/google_map.md b/docs/google_map.md new file mode 100644 index 0000000000..7c6e3aca6c --- /dev/null +++ b/docs/google_map.md @@ -0,0 +1,54 @@ +The webpage is a submodule to this repository and config related to that is in ./web folder + +[OpenPoGoWeb] (https://github.com/OpenPoGo/OpenPoGoWeb) uses Google Maps. Read their [README] (https://github.com/OpenPoGo/OpenPoGoWeb/blob/master/README.md) for how to configure web frontend + +## How to set up a simple webserver with nginx +## SimpleHTTPServer +You can either view the map via opening the html file, or by serving it with SimpleHTTPServer (runs on localhost:8000) +To use SimpleHTTPServer: +``` +$ python -m SimpleHTTPServer [port] +``` +The default port is 8000, you can change that by giving a port number. Anything above port 1000 does not require root. +You will need to set your username(s) in the userdata.js file before opening, **Copy userdata.js.example to userdata.js** and edit with your favorite text editor. Put your username in the quotes instead of "username" +If using multiple usernames format like this +``` +var users = ["username1","username2"]; +``` +On Windows you can now go to http://127.0.0.1:8000 to see the map + + + +### Nginx on Ubuntu 14.x, 16.x +#### 1. Install nginx on your Ubuntu machine (e.g. on locally or AWS) +``` +sudo apt-get update +sudo apt-get install nginx +``` + +#### 2. Check the webserver +Check if the webserver is running by using your browser and entering the IP address of your local machine/server. +On a local machine this would be http://127.0.0.1. On AWS this is your public DNS if you haven't configured an elastic IP. + +#### 3. Change Base Directory of the Webserver +``` +sudo nano "/etc/nginx/sites-enabled/default" +``` +Comment out following line: ```root /var/www/html;``` and change it to the web folder of your PokemonGo-Bot: eg: +``` +/home/user/dev/PokemonGo-Bot/web; +``` +Use `nginx -s reload` to load the new configurations. + + +*** +Common Errors and Solutions + +> missing files: 127.0.0.1 - - "GET /catchable-YOURACCOUNT.json 404 +and location-SOMEACCOUNT.json 404 + +just create the file catachable-someaccount@gmail.com.json and put +``` +{} +``` +save and close repeat for other file. (location-SOMEACCOUNT.json) diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000000..8f5f5692e3 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,93 @@ +### Requirements (click each one for install guide) + +- [Python 2.7.x](http://docs.python-guide.org/en/latest/starting/installation/) +- [pip](https://pip.pypa.io/en/stable/installing/) +- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) +- [docker](https://docs.docker.com/engine/installation/) (Optional) - [how to setup after installation](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/How-to-run-with-Docker) +- [protobuf 3](https://github.com/google/protobuf) (OS Dependent, see below) + +### Protobuf 3 installation + +- OS X: `brew update && brew install --devel protobuf` +- Windows: Download protobuf 3.0: [here](https://github.com/google/protobuf/releases/download/v3.0.0-beta-4/protoc-3.0.0-beta-4-win32.zip) and unzip `bin/protoc.exe` into a folder in your PATH. +- Linux: `apt-get install python-protobuf` + +### Get encrypt.so (Windows part writing need fine tune) +We don't have the copyright of encrypt.so, please grab from internet and build your self.Take the risk as your own. +Example build sequence: +Create a new separate folder some here + +wget http://pgoapi.com/pgoencrypt.tar.gz && tar -xf pgoencrypt.tar.gz && cd pgoencrypt/src/ && make +Then copy libencrypt.so to the gofbot folder and rename to encrypt.so + +### Note on branch +Please keep in mind that master is not always up-to-date whereas 'dev' is. In the installation note below change `master` to `dev` if you want to get and use the latest version. + +## Update +To update your project do (in the project folder): `git pull` + +To update python requirement packages do (in the project folder): `pip install --upgrade -r requirements.txt` + +### Installation Linux +(change master to dev for the latest version) + +``` +$ git clone --recursive -b master https://github.com/PokemonGoF/PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ source bin/activate +$ pip install -r requirements.txt +``` +#### Example Installation for Ubuntu +(change dev to master for the lastest master version) + +http://pastebin.com/pzPjXT65 + + +### Installation Mac +(change master to dev for the latest version) + +``` +$ git clone --recursive -b master https://github.com/PokemonGoF/PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ source bin/activate +$ pip install -r requirements.txt +``` + +### Installation Windows +(change master to dev for the latest version) + +On Windows, you will need to install PyYaml through the installer and not through requirements.txt. + +##### Windows vista, 7, 8: +Go to : http://pyyaml.org/wiki/PyYAML , download the right version for your pc and install it + +##### Windows 10: +Go to [this](http://www.lfd.uci.edu/~gohlke/pythonlibs/#pyyaml) page and download: PyYAML-3.11-cp27-cp27m-win32.whl +(If running 64-bit python or if you get a 'not a supported wheel on this platform' error, +download the 64 bit version instead: PyYAML-3.11-cp27-cp27m-win_amd64.whl ) + +*(Run the following commands from Git Bash.)* + +``` +// switch to the directory where you downloaded PyYAML +$ cd download-directory +// install 32-bit version +$ pip2 install PyYAML-3.11-cp27-cp27m-win32.whl +// if you need to install the 64-bit version, do this instead: +// pip2 install PyYAML-3.11-cp27-cp27m-win_amd64.whl +``` + +After this, just do: + +``` +$ git clone -b master https://github.com/PokemonGoF/PokemonGo-Bot +$ cd PokemonGo-Bot +$ virtualenv . +$ script\activate +$ pip2 install -r requirements.txt +$ git submodule init +$ git submodule update +``` diff --git a/docs/pokemon_iv.md b/docs/pokemon_iv.md new file mode 100644 index 0000000000..8662314fd5 --- /dev/null +++ b/docs/pokemon_iv.md @@ -0,0 +1,58 @@ +Individual Values, or IVs function like a Pokémon's "Genes". They are the traits which are passed down from one generation to the next. + +![](http://vignette3.wikia.nocookie.net/pokemon/images/d/dd/ImagesCAD6WL01.jpg/revision/latest?cb=20110511020243) + +**Individual values** + +Every stat has an IV ranging from 0 to 31 for each stat (HP, ATK, DEF, SPA, SPD, and SPE), and at level 100, their IVs are added to the Pokémon's stats for their total values. For example, a level 100 Tyranitar with no Effort Values and 0 IVs has 310 HP, however if it had 31 IVs, it would have 341 HP. + + +These stats are provided randomly for every Pokémon, caught or bred, and although as insignificant as 31 points may seem, they are required for Ace Trainers to obtain when breeding Pokémon with perfect natures/stats. On some occasions they are even the tipping point in a close matchup. For example, if there was a Terrakion with 0 Attack IV, it will have an attack of 358 at level 100 (with an attack improving nature), while a Terrakion with perfect Attack IVs would have 392 Attack. This small difference can mean the difference between a one-hit kill (not an OHKO) and survival with 1 HP. + + +**Breeding IVs** +Fortunately for trainers, Ace Trainers and Pokémon Breeders especially, IVs can be bred to obtain the perfect Pokémon. + +The process of breeding IVs is as follows, the example displayed below is to breed Nidorans: + +* The child's IV's are generated randomly, for example: 7/27/31/14/19/2, in HP/ATK/DEF/SPA/SPD/SPE format. +* Three stats are inherited from the parents, and are selected in three checks: +1. First check: A random stat (HP/ATK/DEF/SPA/SPD/SPE) is selected from either the Mother or the Father and passed on to the child. +2. Second check: A random stat with the exception of HP (ATK/DEF/SPA/SPD/SPE) is selected from either the Mother or the Father and passed on to the child. +3. Third check: A random stat with the exception of HP and DEF (ATK/SPA/SPD/SPE) is selected from either the Mother or the Father and passed on to the child. +This means that HP and DEF are less likely to pass on to the child, however there are ways to make sure the IVs are passed on. + +Letting either one of the parents hold a Power Item can ensure that the Power Item's respective stat will be passed on to the offspring from the parent that holds it. + +If the Power Item called Power Weight (doubles all HP EV gained) is held by a parent with a perfect IV of 31 for HP and the first check selects this parent, the child is ensured to have a perfect IV for HP. The other checks, though, will be random, and either luck or patience is required to eventually get the desired stats. + +Important: Only three stats are inherited per Pokémon, and these can stack. For example, the DEF IV can be inherited from both parents, thus rendering one redundant. + + + +**Checking IVs** +Beginning in Generation III, there has always been an NPC that allows players to check the IVs of their Pokémon. + + +If you wanted to check the IV's yourself the formula is as follows: + +The formula for HP is different from the rest of the stats, so here is the formula for HP: + +> IV=((Stat - Level Value - 10) * 100 / Level Value) - 2 * Base stat - (Math.Floor(EV/4)) + +In layman terms: + +> Individual Value= ((Current Stat Level - Current Level Value - 10) * 100 / Current Level Value) - 2 * Base Stat - (Math.Floor(EV/4)) + +Just in case you don't know (Math.Floor(EV/4)) means to take the amount of EVs you have in HP and divide it by 4 and then round down. +The formula you use for the rest of the stats is the same, so here it is: + +> IV=((Math.Ceiling(Stat/Nature) - 5) * 100 / Level Value) - 2 * Base Stat - (Math.Floor(EV/4)) + +In layman terms: + +> Individual Value= (Math.Ceiling(Current Stat Value/Nature Bonus) * 100 / Current Level Value) - 2 * Base Stat - (Math.Floor(EV/4)) + +Just in case you don't know (Math.Floor(EV/4)) means to take the amount of EVs you have in HP and divide it by 4 and then round down. + +Just in case you don't know (Math.Ceiling(Current Stat Value/Nature Bonus)) means to take the Current Stat Value and divide it by the bonus you get from the Pokémon's nature and then round up. If the stat gets an increase from the nature you divide the Current Stat Value by 1.1, and if it is a decrease from the nature you divide the Current Stat Value by 0.9. diff --git a/install.sh b/install.sh index 32cc1e1124..1e7415ed30 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -pokebotpath=$(pwd) +pokebotpath=$(cd "$(dirname "$0")"; pwd) cd $pokebotpath if [ -f /etc/debian_version ] then @@ -17,7 +17,9 @@ echo "You are on Mac os" sudo brew update sudo brew install --devel protobuf else -echo "Nothing happend." +echo "Please check if you have python pip protobuf gcc make installed on your device." +echo "Wait 5 seconds to continue or Use ctrl+c to interrupt this shell." +sleep 5 fi pip install virtualenv cd $pokebotpath @@ -36,7 +38,7 @@ mv libencrypt.so $pokebotpath/encrypt.so cd ../.. rm -rf pgoencrypt.tar.gz rm -rf pgoencrypt -echo "Install complete." +echo "Install complete. Starting to generate config.json." cd $pokebotpath read -p "1.google 2.ptc " auth @@ -58,5 +60,5 @@ sed -i "s/YOUR_USERNAME/$username/g" configs/config.json sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json sed -i "s/SOME_LOCATION/$location/g" configs/config.json sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json -echo "Edit configs/config.json to modify any other config. Use run.sh to run." +echo "Edit configs/config.json to modify any other config. Use run.sh ./configs/config.json to run." exit 0 diff --git a/pokecli.py b/pokecli.py index 55afa55399..f59ae3acb9 100644 --- a/pokecli.py +++ b/pokecli.py @@ -39,6 +39,7 @@ from geopy.exc import GeocoderQuotaExceeded from pokemongo_bot import PokemonGoBot, TreeConfigBuilder +from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.health_record import BotEvent from pokemongo_bot.plugin_loader import PluginLoader @@ -162,7 +163,7 @@ def report_summary(bot): def init_config(): parser = argparse.ArgumentParser() - config_file = "configs/config.json" + config_file = os.path.join(_base_dir, 'configs', 'config.json') web_dir = "web" # If config file exists, load variables from json @@ -394,6 +395,14 @@ def _json_loader(filename): type=float, default=5.0 ) + add_config( + parser, + load, + long_flag="--logging_color", + help="If logging_color is set to true, colorized logging handler will be used", + type=bool, + default=True + ) # Start to parse other attrs config = parser.parse_args() diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index dd4de22551..1b18ac7ff8 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -25,9 +25,10 @@ from human_behaviour import sleep from item_list import Item from metrics import Metrics -from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler +from pokemongo_bot.event_handlers import LoggingHandler, SocketIoHandler, ColoredLoggingHandler from pokemongo_bot.socketio_server.runner import SocketIoRunner from pokemongo_bot.websocket_remote_control import WebsocketRemoteControl +from pokemongo_bot.base_dir import _base_dir from worker_result import WorkerResult from tree_config_builder import ConfigException, MismatchTaskApiVersion, TreeConfigBuilder from inventory import init_inventory @@ -57,9 +58,9 @@ def __init__(self, config): self.config = config self.fort_timeouts = dict() self.pokemon_list = json.load( - open(os.path.join('data', 'pokemon.json')) + open(os.path.join(_base_dir, 'data', 'pokemon.json')) ) - self.item_list = json.load(open(os.path.join('data', 'items.json'))) + self.item_list = json.load(open(os.path.join(_base_dir, 'data', 'items.json'))) self.metrics = Metrics(self) self.latest_inventory = None self.cell = None @@ -87,7 +88,12 @@ def start(self): random.seed() def _setup_event_system(self): - handlers = [LoggingHandler()] + handlers = [] + if self.config.logging_color: + handlers.append(ColoredLoggingHandler()) + else: + handlers.append(LoggingHandler()) + if self.config.websocket_server_url: if self.config.websocket_start_embedded_server: self.sio_runner = SocketIoRunner(self.config.websocket_server_url) @@ -102,19 +108,18 @@ def _setup_event_system(self): if self.config.websocket_remote_control: remote_control = WebsocketRemoteControl(self).start() - self.event_manager = EventManager(*handlers) self._register_events() if self.config.show_events: self.event_manager.event_report() sys.exit(1) - # Registering event: - # self.event_manager.register_event("location", parameters=['lat', 'lng']) - # - # Emitting event should be enough to add logging and send websocket - # message: : - # self.event_manager.emit('location', 'level'='info', data={'lat': 1, 'lng':1}), + # Registering event: + # self.event_manager.register_event("location", parameters=['lat', 'lng']) + # + # Emitting event should be enough to add logging and send websocket + # message: : + # self.event_manager.emit('location', 'level'='info', data={'lat': 1, 'lng':1}), def _register_events(self): self.event_manager.register_event( @@ -233,6 +238,10 @@ def _register_events(self): 'cp', 'iv', 'iv_display', + 'encounter_id', + 'latitude', + 'longitude', + 'pokemon_id' ) ) self.event_manager.register_event('no_pokeballs') @@ -267,7 +276,13 @@ def _register_events(self): ) self.event_manager.register_event( 'pokemon_vanished', - parameters=('pokemon',) + parameters=( + 'pokemon', + 'encounter_id', + 'latitude', + 'longitude', + 'pokemon_id' + ) ) self.event_manager.register_event('pokemon_not_in_range') self.event_manager.register_event('pokemon_inventory_full') @@ -275,7 +290,11 @@ def _register_events(self): 'pokemon_caught', parameters=( 'pokemon', - 'cp', 'iv', 'iv_display', 'exp' + 'cp', 'iv', 'iv_display', 'exp', + 'encounter_id', + 'latitude', + 'longitude', + 'pokemon_id' ) ) self.event_manager.register_event( @@ -498,12 +517,12 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): location = self.position[0:2] cells = self.find_close_cells(*location) - user_data_cells = "data/cells-%s.json" % self.config.username + user_data_cells = os.path.join(_base_dir, 'data', 'cells-%s.json' % self.config.username) with open(user_data_cells, 'w') as outfile: json.dump(cells, outfile) user_web_location = os.path.join( - 'web', 'location-%s.json' % self.config.username + _base_dir, 'web', 'location-%s.json' % self.config.username ) # alt is unused atm but makes using *location easier try: @@ -518,7 +537,7 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): self.logger.info('[x] Error while opening location file: %s' % e) user_data_lastlocation = os.path.join( - 'data', 'last-location-%s.json' % self.config.username + _base_dir, 'data', 'last-location-%s.json' % self.config.username ) try: with open(user_data_lastlocation, 'w') as outfile: @@ -790,7 +809,7 @@ def current_inventory(self): inventory_dict = inventory_req['responses']['GET_INVENTORY'][ 'inventory_delta']['inventory_items'] - user_web_inventory = 'web/inventory-%s.json' % self.config.username + user_web_inventory = os.path.join(_base_dir, 'web', 'inventory-%s.json' % self.config.username) with open(user_web_inventory, 'w') as outfile: json.dump(inventory_dict, outfile) @@ -901,8 +920,8 @@ def _set_starting_position(self): level='debug', formatted='Loading cached location...' ) - with open('data/last-location-%s.json' % - self.config.username) as f: + with open(os.path.join(_base_dir, 'data', 'last-location-%s.json' % + self.config.username)) as f: location_json = json.load(f) location = ( location_json['lat'], @@ -987,9 +1006,10 @@ def heartbeat(self): pass def update_web_location_worker(self): - while True: - self.web_update_queue.get() - self.update_web_location() + pass + # while True: + # self.web_update_queue.get() + # self.update_web_location() def get_inventory_count(self, what): response_dict = self.get_inventory() @@ -1051,8 +1071,8 @@ def has_space_for_loot(self): def get_forts(self, order_by_distance=False): forts = [fort - for fort in self.cell['forts'] - if 'latitude' in fort and 'type' in fort] + for fort in self.cell['forts'] + if 'latitude' in fort and 'type' in fort] if order_by_distance: forts.sort(key=lambda x: distance( diff --git a/pokemongo_bot/base_dir.py b/pokemongo_bot/base_dir.py new file mode 100644 index 0000000000..83978c964a --- /dev/null +++ b/pokemongo_bot/base_dir.py @@ -0,0 +1,4 @@ +import os + + +_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') diff --git a/pokemongo_bot/base_task.py b/pokemongo_bot/base_task.py index 22bbedf4e8..1b610d31aa 100644 --- a/pokemongo_bot/base_task.py +++ b/pokemongo_bot/base_task.py @@ -9,6 +9,7 @@ def __init__(self, bot, config): self.config = config self._validate_work_exists() self.logger = logging.getLogger(type(self).__name__) + self.enabled = config.get('enabled', True) self.initialize() def _validate_work_exists(self): diff --git a/pokemongo_bot/cell_workers/catch_visible_pokemon.py b/pokemongo_bot/cell_workers/catch_visible_pokemon.py index 654c2467b3..0203459c28 100644 --- a/pokemongo_bot/cell_workers/catch_visible_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_visible_pokemon.py @@ -1,9 +1,11 @@ import json +import os from pokemongo_bot.base_task import BaseTask from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker from utils import distance from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.base_dir import _base_dir class CatchVisiblePokemon(BaseTask): @@ -27,7 +29,7 @@ def work(self): key= lambda x: distance(self.bot.position[0], self.bot.position[1], x['latitude'], x['longitude']) ) - user_web_catchable = 'web/catchable-{}.json'.format(self.bot.config.username) + user_web_catchable = os.path.join(_base_dir, 'web', 'catchable-{}.json'.format(self.bot.config.username)) for pokemon in self.bot.cell['catchable_pokemons']: with open(user_web_catchable, 'w') as outfile: json.dump(pokemon, outfile) diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 4cc451115b..7380f1c5db 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -117,8 +117,3 @@ def _execute_pokemon_evolve(self, pokemon, cache): cache[pokemon.name] = 1 sleep(0.7) return False - - def _compute_iv(self, pokemon): - total_iv = pokemon.get("individual_attack", 0) + pokemon.get("individual_stamina", 0) + pokemon.get( - "individual_defense", 0) - return round((total_iv / 45.0), 2) diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 7dcd0977b1..24ecf5e74a 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -13,13 +13,17 @@ class MoveToFort(BaseTask): def initialize(self): self.lure_distance = 0 - self.lure_attraction = self.config.get("lure_attraction", True) - self.lure_max_distance = self.config.get("lure_max_distance", 2000) - self.ignore_item_count = self.config.get("ignore_item_count", False) + if self.config: + self.lure_attraction = self.config.get("lure_attraction", True) + self.lure_max_distance = self.config.get("lure_max_distance", 2000) + self.ignore_item_count = self.config.get("ignore_item_count", False) + else: + self.lure_attraction = None + self.ignore_item_count = True def should_run(self): has_space_for_loot = self.bot.has_space_for_loot() - if not has_space_for_loot: + if not has_space_for_loot and not self.ignore_item_count: self.emit_event( 'inventory_full', formatted="Inventory is full. You might want to change your config to recycle more items if this message appears consistently." diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 2cfd45d14b..efd058ca96 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -54,6 +54,7 @@ import json import base64 import requests +from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.cell_workers.utils import distance, format_dist, format_time from pokemongo_bot.step_walker import StepWalker from pokemongo_bot.worker_result import WorkerResult @@ -83,8 +84,9 @@ def initialize(self): self.unit = self.bot.config.distance_unit self.caught = [] self.min_ball = self.config.get('min_ball', 1) + self.map_path = self.config.get('map_path', 'raw_data') - data_file = 'data/map-caught-{}.json'.format(self.bot.config.username) + data_file = os.path.join(_base_dir, 'map-caught-{}.json'.format(self.bot.config.username)) if os.path.isfile(data_file): self.caught = json.load( open(data_file) @@ -92,7 +94,7 @@ def initialize(self): def get_pokemon_from_map(self): try: - req = requests.get('{}/raw_data?gyms=false&scanned=false'.format(self.config['address'])) + req = requests.get('{}/{}?gyms=false&scanned=false'.format(self.config['address'], self.map_path)) except requests.exceptions.ConnectionError: self._emit_failure('Could not get Pokemon data from PokemonGo-Map: ' '{}. Is it running?'.format( @@ -222,7 +224,7 @@ def snipe(self, pokemon): return WorkerResult.SUCCESS def dump_caught_pokemon(self): - user_data_map_caught = 'data/map-caught-{}.json'.format(self.bot.config.username) + user_data_map_caught = os.path.join(_base_dir, 'data', 'map-caught-{}.json'.format(self.bot.config.username)) with open(user_data_map_caught, 'w') as outfile: json.dump(self.caught, outfile) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index d551a68632..3b2092e535 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -98,6 +98,10 @@ def work(self, response_dict=None): 'cp': pokemon.cp, 'iv': pokemon.iv, 'iv_display': pokemon.iv_display, + 'encounter_id': self.pokemon['encounter_id'], + 'latitude': self.pokemon['latitude'], + 'longitude': self.pokemon['longitude'], + 'pokemon_id': pokemon.num } ) @@ -247,7 +251,7 @@ def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, cu data={ 'berry_name': self.item_list[str(berry_id)], 'ball_name': self.item_list[str(current_ball)], - 'new_catch_rate': self._pct(catch_rate_by_ball[current_ball]) + 'new_catch_rate': self._pct(new_catch_rate_by_ball[current_ball]) } ) @@ -370,7 +374,13 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): self.emit_event( 'pokemon_vanished', formatted='{pokemon} vanished!', - data={'pokemon': pokemon.name} + data={ + 'pokemon': pokemon.name, + 'encounter_id': self.pokemon['encounter_id'], + 'latitude': self.pokemon['latitude'], + 'longitude': self.pokemon['longitude'], + 'pokemon_id': pokemon.num + } ) if self._pct(catch_rate_by_ball[current_ball]) == 100: self.bot.softban = True @@ -386,7 +396,11 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): 'cp': pokemon.cp, 'iv': pokemon.iv, 'iv_display': pokemon.iv_display, - 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']) + 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']), + 'encounter_id': self.pokemon['encounter_id'], + 'latitude': self.pokemon['latitude'], + 'longitude': self.pokemon['longitude'], + 'pokemon_id': pokemon.num } ) diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 673b373fba..3232870d03 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -1,5 +1,6 @@ import json import os +from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.base_task import BaseTask from pokemongo_bot.tree_config_builder import ConfigException @@ -13,7 +14,7 @@ def initialize(self): self._validate_item_filter() def _validate_item_filter(self): - item_list = json.load(open(os.path.join('data', 'items.json'))) + item_list = json.load(open(os.path.join(_base_dir, 'data', 'items.json'))) for config_item_name, bag_count in self.item_filter.iteritems(): if config_item_name not in item_list.viewvalues(): if config_item_name not in item_list: @@ -27,7 +28,7 @@ def work(self): free_bag_space = total_bag_space - items_in_bag if self.min_empty_space is not None: - if free_bag_space >= self.min_empty_space: + if free_bag_space >= self.min_empty_space and items_in_bag < total_bag_space: self.emit_event( 'item_discard_skipped', formatted="Skipping Recycling of Items. {space} space left in bag.", diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 445946e7e1..9422d8ec35 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -19,7 +19,7 @@ def initialize(self): self.ignore_item_count = self.config.get("ignore_item_count", False) def should_run(self): - if not self.bot.has_space_for_loot(): + if not self.bot.has_space_for_loot() and not self.ignore_item_count: self.emit_event( 'inventory_full', formatted="Inventory is full. You might want to change your config to recycle more items if this message appears consistently." @@ -106,10 +106,11 @@ def work(self): data={'pokestop': fort_name, 'minutes_left': minutes_left} ) elif spin_result == 4: - self.emit_event( - 'inventory_full', - formatted="Inventory is full!" - ) + if not self.ignore_item_count: + self.emit_event( + 'inventory_full', + formatted="Inventory is full!" + ) else: self.emit_event( 'unknown_spin_result', diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index ebc197ef24..9e970d7d7f 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -1,7 +1,10 @@ import json +import os +from pokemongo_bot import inventory from pokemongo_bot.human_behaviour import action_delay from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.inventory import Pokemons class TransferPokemon(BaseTask): @@ -9,129 +12,75 @@ class TransferPokemon(BaseTask): def work(self): pokemon_groups = self._release_pokemon_get_groups() - for pokemon_id in pokemon_groups: - group = pokemon_groups[pokemon_id] - - if len(group) > 0: - pokemon_name = self.bot.pokemon_list[pokemon_id - 1]['Name'] - keep_best, keep_best_cp, keep_best_iv = self._validate_keep_best_config(pokemon_name) - - if keep_best: - best_pokemon_ids = set() - order_criteria = 'none' - if keep_best_cp >= 1: - cp_limit = keep_best_cp - best_cp_pokemons = sorted(group, key=lambda x: (x['cp'], x['iv']), reverse=True)[:cp_limit] - best_pokemon_ids = set(pokemon['pokemon_data']['id'] for pokemon in best_cp_pokemons) - order_criteria = 'cp' - - if keep_best_iv >= 1: - iv_limit = keep_best_iv - best_iv_pokemons = sorted(group, key=lambda x: (x['iv'], x['cp']), reverse=True)[:iv_limit] - best_pokemon_ids |= set(pokemon['pokemon_data']['id'] for pokemon in best_iv_pokemons) - if order_criteria == 'cp': - order_criteria = 'cp and iv' - else: - order_criteria = 'iv' - - # remove best pokemons from all pokemons array - all_pokemons = group - best_pokemons = [] - for best_pokemon_id in best_pokemon_ids: - for pokemon in all_pokemons: - if best_pokemon_id == pokemon['pokemon_data']['id']: - all_pokemons.remove(pokemon) - best_pokemons.append(pokemon) - - transfer_pokemons = [pokemon for pokemon in all_pokemons - if self.should_release_pokemon(pokemon_name, - pokemon['cp'], - pokemon['iv'], - True)] - - if transfer_pokemons: - if best_pokemons: - self.emit_event( - 'keep_best_release', - formatted="Keeping best {amount} {pokemon}, based on {criteria}", - data={ - 'amount': len(best_pokemons), - 'pokemon': pokemon_name, - 'criteria': order_criteria - } - ) - for pokemon in transfer_pokemons: - self.release_pokemon(pokemon_name, pokemon['cp'], pokemon['iv'], pokemon['pokemon_data']['id']) - else: - group = sorted(group, key=lambda x: x['cp'], reverse=True) - for item in group: - pokemon_cp = item['cp'] - pokemon_potential = item['iv'] - - if self.should_release_pokemon(pokemon_name, pokemon_cp, pokemon_potential): - self.release_pokemon(pokemon_name, item['cp'], item['iv'], item['pokemon_data']['id']) + for pokemon_id, group in pokemon_groups.iteritems(): + pokemon_name = Pokemons.name_for(pokemon_id) + keep_best, keep_best_cp, keep_best_iv = self._validate_keep_best_config(pokemon_name) + + if keep_best: + best_pokemon_ids = set() + order_criteria = 'none' + if keep_best_cp >= 1: + cp_limit = keep_best_cp + best_cp_pokemons = sorted(group, key=lambda x: (x.cp, x.iv), reverse=True)[:cp_limit] + best_pokemon_ids = set(pokemon.id for pokemon in best_cp_pokemons) + order_criteria = 'cp' + + if keep_best_iv >= 1: + iv_limit = keep_best_iv + best_iv_pokemons = sorted(group, key=lambda x: (x.iv, x.cp), reverse=True)[:iv_limit] + best_pokemon_ids |= set(pokemon.id for pokemon in best_iv_pokemons) + if order_criteria == 'cp': + order_criteria = 'cp and iv' + else: + order_criteria = 'iv' + + # remove best pokemons from all pokemons array + all_pokemons = group + best_pokemons = [] + for best_pokemon_id in best_pokemon_ids: + for pokemon in all_pokemons: + if best_pokemon_id == pokemon.id: + all_pokemons.remove(pokemon) + best_pokemons.append(pokemon) + + transfer_pokemons = [pokemon for pokemon in all_pokemons if self.should_release_pokemon(pokemon,True)] + + if transfer_pokemons: + if best_pokemons: + self.emit_event( + 'keep_best_release', + formatted="Keeping best {amount} {pokemon}, based on {criteria}", + data={ + 'amount': len(best_pokemons), + 'pokemon': pokemon_name, + 'criteria': order_criteria + } + ) + for pokemon in transfer_pokemons: + self.release_pokemon(pokemon) + else: + group = sorted(group, key=lambda x: x.cp, reverse=True) + for pokemon in group: + if self.should_release_pokemon(pokemon): + self.release_pokemon(pokemon) def _release_pokemon_get_groups(self): pokemon_groups = {} - request = self.bot.api.create_request() - request.get_player() - request.get_inventory() - inventory_req = request.call() - - if inventory_req.get('responses', False) is False: - return pokemon_groups - - inventory_dict = inventory_req['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] - - user_web_inventory = 'web/inventory-%s.json' % (self.bot.config.username) - with open(user_web_inventory, 'w') as outfile: - json.dump(inventory_dict, outfile) - - for pokemon in inventory_dict: - try: - reduce(dict.__getitem__, [ - "inventory_item_data", "pokemon_data", "pokemon_id" - ], pokemon) - except KeyError: - continue - - pokemon_data = pokemon['inventory_item_data']['pokemon_data'] - - # pokemon in fort, so we cant transfer it - if 'deployed_fort_id' in pokemon_data and pokemon_data['deployed_fort_id']: - continue - - # favorite pokemon can't transfer in official game client - if pokemon_data.get('favorite', 0) is 1: + for pokemon in inventory.pokemons(True).all(): + if pokemon.in_fort or pokemon.is_favorite: continue - group_id = pokemon_data['pokemon_id'] - group_pokemon_cp = pokemon_data['cp'] - group_pokemon_iv = self.get_pokemon_potential(pokemon_data) + group_id = pokemon.pokemon_id if group_id not in pokemon_groups: pokemon_groups[group_id] = [] - pokemon_groups[group_id].append({ - 'cp': group_pokemon_cp, - 'iv': group_pokemon_iv, - 'pokemon_data': pokemon_data - }) + pokemon_groups[group_id].append(pokemon) return pokemon_groups - def get_pokemon_potential(self, pokemon_data): - total_iv = 0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] - for individual_stat in iv_stats: - try: - total_iv += pokemon_data[individual_stat] - except Exception: - continue - return round((total_iv / 45.0), 2) - - def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): - release_config = self._get_release_config_for(pokemon_name) + def should_release_pokemon(self, pokemon, keep_best_mode = False): + release_config = self._get_release_config_for(pokemon.name) if (keep_best_mode and not release_config.has_key('never_release') @@ -156,11 +105,11 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): return True release_cp = release_config.get('release_below_cp', 0) - if cp < release_cp: + if pokemon.cp < release_cp: release_results['cp'] = True release_iv = release_config.get('release_below_iv', 0) - if iv < release_iv: + if pokemon.iv < release_iv: release_results['iv'] = True logic_to_function = { @@ -171,11 +120,11 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): if logic_to_function[cp_iv_logic](*release_results.values()): self.emit_event( 'future_pokemon_release', - formatted="Releasing {pokemon} (CP {cp}/IV {iv}) based on rule: CP < {below_cp} {cp_iv_logic} IV < {below_iv}", + formatted="Releasing {pokemon} [CP {cp}] [IV {iv}] based on rule: CP < {below_cp} {cp_iv_logic} IV < {below_iv}", data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': iv, + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv, 'below_cp': release_cp, 'cp_iv_logic': cp_iv_logic.upper(), 'below_iv': release_iv @@ -184,16 +133,27 @@ def should_release_pokemon(self, pokemon_name, cp, iv, keep_best_mode = False): return logic_to_function[cp_iv_logic](*release_results.values()) - def release_pokemon(self, pokemon_name, cp, iv, pokemon_id): - response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon_id) + def release_pokemon(self, pokemon): + try: + if self.bot.config.test: + candy_awarded = 1 + else: + response_dict = self.bot.api.release_pokemon(pokemon_id=pokemon.id) + candy_awarded = response_dict['responses']['RELEASE_POKEMON']['candy_awarded'] + except KeyError: + return + + # We could refresh here too, but adding 1 saves a inventory request + candy = inventory.candies().get(pokemon.pokemon_id) + candy.add(candy_awarded) self.bot.metrics.released_pokemon() self.emit_event( 'pokemon_release', formatted='Exchanged {pokemon} [CP {cp}] [IV {iv}] for candy.', data={ - 'pokemon': pokemon_name, - 'cp': cp, - 'iv': iv + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv } ) action_delay(self.bot.config.action_wait_min, self.bot.config.action_wait_max) diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_title_stats.py index 0a6e3c592d..bc40ed82e8 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_title_stats.py @@ -18,10 +18,22 @@ class UpdateTitleStats(BaseTask): "type": "UpdateTitleStats", "config": { "min_interval": 10, - "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"] + "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"], } } + You can set a logging on terminal mode like this: + + Example logging on console (and disabling title change): + { + "type": "UpdateTitleStats", + "config": { + "min_interval": 10, + "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"], + "terminal_log": true, + "terminal_title": false + } + } Available stats : - login : The account login (from the credentials). - username : The trainer name (asked at first in-game connection). @@ -53,8 +65,6 @@ class UpdateTitleStats(BaseTask): """ SUPPORTED_TASK_API_VERSION = 1 - DEFAULT_MIN_INTERVAL = 10 - DEFAULT_DISPLAYED_STATS = [] def __init__(self, bot, config): """ @@ -67,12 +77,14 @@ def __init__(self, bot, config): super(UpdateTitleStats, self).__init__(bot, config) self.next_update = None - self.min_interval = self.DEFAULT_MIN_INTERVAL - self.displayed_stats = self.DEFAULT_DISPLAYED_STATS - self.bot.event_manager.register_event('update_title', parameters=('title')) + self.min_interval = int(self.config.get('min_interval', 120)) + self.displayed_stats = self.config.get('stats', []) + self.terminal_log = self.config.get('terminal_log', False) + self.terminal_title = self.config.get('terminal_title', True) - self._process_config() + self.bot.event_manager.register_event('update_title', parameters=('title',)) + self.bot.event_manager.register_event('log_stats',parameters=('title',)) def initialize(self): pass @@ -89,7 +101,12 @@ def work(self): # If title is empty, it couldn't be generated. if not title: return WorkerResult.SUCCESS - self._update_title(title, _platform) + + if self.terminal_title: + self._update_title(title, _platform) + + if self.terminal_log: + self._log_on_terminal(title) return WorkerResult.SUCCESS def _should_display(self): @@ -100,6 +117,16 @@ def _should_display(self): """ return self.next_update is None or datetime.now() >= self.next_update + def _log_on_terminal(self, title): + self.emit_event( + 'log_stats', + formatted="{title}", + data={ + 'title': title + } + ) + self.next_update = datetime.now() + timedelta(seconds=self.min_interval) + def _update_title(self, title, platform): """ Updates the window title using different methods, according to the given platform @@ -119,7 +146,7 @@ def _update_title(self, title, platform): 'title': title } ) - + if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) stdout.flush() @@ -133,14 +160,6 @@ def _update_title(self, title, platform): self.next_update = datetime.now() + timedelta(seconds=self.min_interval) - def _process_config(self): - """ - Fetches the configuration for this worker and stores the values internally. - :return: Nothing. - :rtype: None - """ - self.min_interval = int(self.config.get('min_interval', self.DEFAULT_MIN_INTERVAL)) - self.displayed_stats = self.config.get('stats', self.DEFAULT_DISPLAYED_STATS) def _get_stats_title(self, player_stats): """ diff --git a/pokemongo_bot/event_handlers/__init__.py b/pokemongo_bot/event_handlers/__init__.py index f0933a0e68..a4dd6fce7f 100644 --- a/pokemongo_bot/event_handlers/__init__.py +++ b/pokemongo_bot/event_handlers/__init__.py @@ -1,2 +1,3 @@ from logging_handler import LoggingHandler from socketio_handler import SocketIoHandler +from colored_logging_handler import ColoredLoggingHandler diff --git a/pokemongo_bot/event_handlers/colored_logging_handler.py b/pokemongo_bot/event_handlers/colored_logging_handler.py new file mode 100644 index 0000000000..f3c902d464 --- /dev/null +++ b/pokemongo_bot/event_handlers/colored_logging_handler.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import time +import sys +import struct + +from pokemongo_bot.event_manager import EventHandler + +class ColoredLoggingHandler(EventHandler): + EVENT_COLOR_MAP = { + 'api_error': 'red', + 'bot_exit': 'red', + 'bot_start': 'green', + 'config_error': 'red', + 'egg_already_incubating': 'yellow', + 'egg_hatched': 'green', + 'future_pokemon_release': 'yellow', + 'incubate': 'green', + 'incubator_already_used': 'yellow', + 'inventory_full': 'yellow', + 'item_discard_fail': 'red', + 'item_discarded': 'green', + 'keep_best_release': 'green', + 'level_up': 'green', + 'level_up_reward': 'green', + 'location_cache_error': 'yellow', + 'location_cache_ignored': 'yellow', + 'login_failed': 'red', + 'login_successful': 'green', + 'lucky_egg_error': 'red', + 'move_to_map_pokemon_encounter': 'green', + 'move_to_map_pokemon_fail': 'red', + 'next_egg_incubates': 'yellow', + 'next_sleep': 'green', + 'no_pokeballs': 'red', + 'pokemon_appeared': 'yellow', + 'pokemon_capture_failed': 'red', + 'pokemon_caught': 'blue', + 'pokemon_evolved': 'green', + 'pokemon_fled': 'red', + 'pokemon_inventory_full': 'red', + 'pokemon_nickname_invalid': 'red', + 'pokemon_not_in_range': 'yellow', + 'pokemon_release': 'green', + 'pokemon_vanished': 'red', + 'pokestop_empty': 'yellow', + 'pokestop_searching_too_often': 'yellow', + 'rename_pokemon': 'green', + 'skip_evolve': 'yellow', + 'softban': 'red', + 'spun_pokestop': 'cyan', + 'threw_berry_failed': 'red', + 'unknown_spin_result': 'red', + 'unset_pokemon_nickname': 'red', + 'vip_pokemon': 'red', + + # event names for 'white' still here to remember that these events are already determined its color. + 'arrived_at_cluster': 'white', + 'arrived_at_fort': 'white', + 'bot_sleep': 'white', + 'catchable_pokemon': 'white', + 'found_cluster': 'white', + 'incubate_try': 'white', + 'load_cached_location': 'white', + 'location_found': 'white', + 'login_started': 'white', + 'lured_pokemon_found': 'white', + 'move_to_map_pokemon_move_towards': 'white', + 'move_to_map_pokemon_teleport_back': 'white', + 'move_to_map_pokemon_updated_map': 'white', + 'moving_to_fort': 'white', + 'moving_to_lured_fort': 'white', + 'pokemon_catch_rate': 'white', + 'pokemon_evolve_fail': 'white', + 'pokestop_on_cooldown': 'white', + 'pokestop_out_of_range': 'white', + 'polyline_request': 'white', + 'position_update': 'white', + 'set_start_location': 'white', + 'softban_fix': 'white', + 'softban_fix_done': 'white', + 'spun_fort': 'white', + 'threw_berry': 'white', + 'threw_pokeball': 'white', + 'used_lucky_egg': 'white' + } + CONTINUOUS_EVENT_NAMES = [ + 'catchable_pokemon', + 'moving_to_lured_fort', + 'spun_fort' + ] + COLOR_CODE = { + 'red': '91', + 'green': '92', + 'yellow': '93', + 'blue': '94', + 'cyan': '96' + } + + def __init__(self): + self._last_event = None + try: + # this `try ... except` is for ImportError on Windows + import fcntl + import termios + self._ioctl = fcntl.ioctl + self._TIOCGWINSZ = termios.TIOCGWINSZ + except ImportError: + self._ioctl = None + self._TIOCGWINSZ = None + + def handle_event(self, event, sender, level, formatted_msg, data): + # Prepare message string + message = None + if formatted_msg: + try: + message = formatted_msg.decode('utf-8') + except UnicodeEncodeError: + message = formatted_msg + else: + message = '{}'.format(str(data)) + + # Replace message if necessary + if event == 'catchable_pokemon': + message = 'Something rustles nearby!' + + # Truncate previous line if same event continues + if event in ColoredLoggingHandler.CONTINUOUS_EVENT_NAMES and self._last_event == event and sys.stdout.isatty(): + # Filling with "' ' * terminal_width" in order to completely clear last line + terminal_width = self._terminal_width() + if terminal_width: + sys.stdout.write('\r{}\r'.format(' ' * terminal_width)) + else: + sys.stdout.write('\r') + else: + sys.stdout.write("\n") + + color_name = None + if event in ColoredLoggingHandler.EVENT_COLOR_MAP: + color_name = ColoredLoggingHandler.EVENT_COLOR_MAP[event] + + # Change color if necessary + if event == 'egg_hatched' and data.get('pokemon', 'error') == 'error': + # `egg_hatched` event will be dispatched in both cases: hatched pokemon info is successfully taken or not. + # change color from 'green' to 'red' in case of error. + color_name = 'red' + + if color_name in ColoredLoggingHandler.COLOR_CODE: + sys.stdout.write( + '[{time}] \033[{color}m{message}\033[0m'.format( + time=time.strftime("%H:%M:%S"), + color=ColoredLoggingHandler.COLOR_CODE[color_name], + message=message + ) + ) + else: + sys.stdout.write('[{time}] {message}'.format( + time=time.strftime("%H:%M:%S"), + message=message + )) + + sys.stdout.flush() + self._last_event = event + + def _terminal_width(self): + if self._ioctl is None or self._TIOCGWINSZ is None: + return None + + h, w, hp, wp = struct.unpack(str('HHHH'), + self._ioctl(0, self._TIOCGWINSZ, + struct.pack(str('HHHH'), 0, 0, 0, 0))) + return w diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index c74a85296f..d7f890933f 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1,25 +1,46 @@ import json +import logging import os +from pokemongo_bot.base_dir import _base_dir + ''' Helper class for updating/retrieving Inventory data ''' -class _BaseInventoryComponent(object): - TYPE = None # base key name for items of this type - ID_FIELD = None # identifier field for items of this type + +# +# Abstraction + +class _StaticInventoryComponent(object): STATIC_DATA_FILE = None # optionally load static data from file, # dropping the data in a static variable named STATIC_DATA + STATIC_DATA = None def __init__(self): - self._data = {} if self.STATIC_DATA_FILE is not None: self.init_static_data() @classmethod def init_static_data(cls): if not hasattr(cls, 'STATIC_DATA') or cls.STATIC_DATA is None: - cls.STATIC_DATA = json.load(open(cls.STATIC_DATA_FILE)) + cls.STATIC_DATA = cls.process_static_data( + json.load(open(cls.STATIC_DATA_FILE))) + + @classmethod + def process_static_data(cls, data): + # optional hook for processing the static data + # default is to use the data directly + return data + + +class _BaseInventoryComponent(_StaticInventoryComponent): + TYPE = None # base key name for items of this type + ID_FIELD = None # identifier field for items of this type + + def __init__(self): + self._data = {} + super(_BaseInventoryComponent, self).__init__() def parse(self, item): # optional hook for parsing the dict for this item @@ -41,34 +62,22 @@ def retrieve_data(self, inventory): def refresh(self, inventory): self._data = self.retrieve_data(inventory) - def get(self, id): - return self._data(id) + def get(self, object_id): + return self._data.get(object_id) def all(self): return list(self._data.values()) -class Candy(object): - def __init__(self, family_id, quantity): - self.type = Pokemons.name_for(family_id) - self.quantity = quantity - - def consume(self, amount): - if self.quantity < amount: - raise Exception('Tried to consume more {} candy than you have'.format(self.type)) - self.quantity -= amount - - def add(self, amount): - if amount < 0: - raise Exception('Must add positive amount of candy') - self.quantity += amount +# +# Inventory Components class Candies(_BaseInventoryComponent): TYPE = 'candy' ID_FIELD = 'family_id' @classmethod - def family_id_for(self, pokemon_id): + def family_id_for(cls, pokemon_id): return Pokemons.first_evolution_id_for(pokemon_id) def get(self, pokemon_id): @@ -96,7 +105,7 @@ def captured(self, pokemon_id): class Items(_BaseInventoryComponent): TYPE = 'item' ID_FIELD = 'item_id' - STATIC_DATA_FILE = os.path.join('data', 'items.json') + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'items.json') def count_for(self, item_id): return self._data[item_id]['count'] @@ -105,19 +114,122 @@ def count_for(self, item_id): class Pokemons(_BaseInventoryComponent): TYPE = 'pokemon_data' ID_FIELD = 'id' - STATIC_DATA_FILE = os.path.join('data', 'pokemon.json') + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'pokemon.json') - def parse(self, item): - if 'is_egg' in item: - return Egg(item) - return Pokemon(item) + @classmethod + def process_static_data(cls, data): + pokemon_id = 1 + for poke_info in data: + # prepare types + types = [poke_info['Type I'][0]] # required + for t in poke_info.get('Type II', []): + types.append(t) + poke_info['types'] = types + + # prepare attacks (moves) + cls._process_attacks(poke_info) + cls._process_attacks(poke_info, charged=True) + + # prepare movesets + poke_info['movesets'] = cls._process_movesets(poke_info, pokemon_id) + + # calculate maximum CP for the pokemon (best IVs, lvl 40) + base_attack = poke_info['BaseAttack'] + base_defense = poke_info['BaseDefense'] + base_stamina = poke_info['BaseStamina'] + max_cp = _calc_cp(base_attack, base_defense, base_stamina) + poke_info['max_cp'] = max_cp + + pokemon_id += 1 + return data + + @classmethod + def _process_movesets(cls, poke_info, pokemon_id): + # type: (dict, int) -> List[Moveset] + """ + The optimal moveset is the combination of two moves, one quick move + and one charge move, that deals the most damage over time. + + Because each quick move gains a certain amount of energy (different + for different moves) and each charge move requires a different amount + of energy to use, sometimes, a quick move with lower DPS will be + better since it charges the charge move faster. On the same note, + sometimes a charge move that has lower DPS will be more optimal since + it may require less energy or it may last for a longer period of time. + + Attacker have STAB (Same-type attack bonus - x1.25) pokemon have the + same type as attack. So we add it to the "Combo DPS" of the moveset. + + The defender attacks in intervals of 1 second for the first 2 attacks, + and then in intervals of 2 seconds for the remainder of the attacks. + This explains why we see two consecutive quick attacks at the beginning + of the match. As a result, we add +2 seconds to the DPS calculation + for defender DPS output. + + So to determine an optimal defensive moveset, we follow the same method + as we did for optimal offensive movesets, but instead calculate the + highest "Combo DPS" with an added 2 seconds to the quick move cool down. + + Note: critical hits have not yet been implemented in the game + + See http://pokemongo.gamepress.gg/optimal-moveset-explanation + See http://pokemongo.gamepress.gg/defensive-tactics + """ + + # Prepare movesets + movesets = [] + types = poke_info['types'] + for fm in poke_info['Fast Attack(s)']: + for chm in poke_info['Special Attack(s)']: + movesets.append(Moveset(fm, chm, types, pokemon_id)) + assert len(movesets) > 0 + + # Calculate attack perfection for each moveset + movesets = sorted(movesets, key=lambda m: m.dps_attack) + worst_dps = movesets[0].dps_attack + best_dps = movesets[-1].dps_attack + if best_dps > worst_dps: + for moveset in movesets: + current_dps = moveset.dps_attack + moveset.attack_perfection = \ + (current_dps - worst_dps) / (best_dps - worst_dps) + + # Calculate defense perfection for each moveset + movesets = sorted(movesets, key=lambda m: m.dps_defense) + worst_dps = movesets[0].dps_defense + best_dps = movesets[-1].dps_defense + if best_dps > worst_dps: + for moveset in movesets: + current_dps = moveset.dps_defense + moveset.defense_perfection = \ + (current_dps - worst_dps) / (best_dps - worst_dps) + + return sorted(movesets, key=lambda m: m.dps, reverse=True) + + @classmethod + def _process_attacks(cls, poke_info, charged=False): + # type: (dict, bool) -> List[Attack] + key = 'Fast Attack(s)' if not charged else 'Special Attack(s)' + moves_dict = (ChargedAttacks if charged else FastAttacks).BY_NAME + moves = [] + for name in poke_info[key]: + if name not in moves_dict: + raise KeyError('Unknown {} attack: "{}"'.format( + 'charged' if charged else 'fast', name)) + moves.append(moves_dict[name]) + moves = sorted(moves, key=lambda m: m.dps, reverse=True) + poke_info[key] = moves + assert len(moves) > 0 + return moves @classmethod def data_for(cls, pokemon_id): + # type: (int) -> dict return cls.STATIC_DATA[pokemon_id - 1] @classmethod def name_for(cls, pokemon_id): + # type: (int) -> string return cls.data_for(pokemon_id)['Name'] @classmethod @@ -128,24 +240,194 @@ def first_evolution_id_for(cls, pokemon_id): return pokemon_id @classmethod - def next_evolution_id_for(cls, pokemon_id): + def prev_evolution_id_for(cls, pokemon_id): + data = cls.data_for(pokemon_id) + if 'Previous evolution(s)' in data: + return int(data['Previous evolution(s)'][-1]['Number']) + return None + + @classmethod + def next_evolution_ids_for(cls, pokemon_id): try: - return int(cls.data_for(pokemon_id)['Next evolution(s)'][0]['Number']) + next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] except KeyError: - return None + return [] + # get only next level evolutions, not all possible + ids = [] + for p in next_evolutions: + p_id = int(p['Number']) + if cls.prev_evolution_id_for(p_id) == pokemon_id: + ids.append(p_id) + return ids @classmethod - def evolution_cost_for(cls, pokemon_id): + def last_evolution_ids_for(cls, pokemon_id): try: - return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + next_evolutions = cls.data_for(pokemon_id)['Next evolution(s)'] except KeyError: - return + return [pokemon_id] + # get only final evolutions, not all possible + ids = [] + for p in next_evolutions: + p_id = int(p['Number']) + if len(cls.data_for(p_id).get('Next evolution(s)', [])) == 0: + ids.append(p_id) + assert len(ids) > 0 + return ids + + @classmethod + def has_next_evolution(cls, pokemon_id): + poke_info = cls.data_for(pokemon_id) + return 'Next Evolution Requirements' in poke_info \ + or 'Next evolution(s)' in poke_info + + @classmethod + def evolution_cost_for(cls, pokemon_id): + if not cls.has_next_evolution(pokemon_id): + return None + return int(cls.data_for(pokemon_id)['Next Evolution Requirements']['Amount']) + + def parse(self, item): + if 'is_egg' in item: + return Egg(item) + return Pokemon(item) def all(self): # by default don't include eggs in all pokemon (usually just # makes caller's lives more difficult) return [p for p in super(Pokemons, self).all() if not isinstance(p, Egg)] + +# +# Static Components + +class LevelToCPm(_StaticInventoryComponent): + """ + Data for the CP multipliers at different levels + See http://pokemongo.gamepress.gg/cp-multiplier + See https://github.com/justinleewells/pogo-optimizer/blob/edd692d/data/game/level-to-cpm.json + """ + + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'level_to_cpm.json') + MAX_LEVEL = 40 + MAX_CPM = .0 + # half of the lowest difference between CPMs + HALF_DIFF_BETWEEN_HALF_LVL = 14e-3 + + @classmethod + def init_static_data(cls): + super(LevelToCPm, cls).init_static_data() + cls.MAX_CPM = cls.cp_multiplier_for(cls.MAX_LEVEL) + + @classmethod + def cp_multiplier_for(cls, level): + # type: (Union[float, int, string]) -> float + level = float(level) + level = str(int(level) if level.is_integer() else level) + return cls.STATIC_DATA[level] + + @classmethod + def level_from_cpm(cls, cp_multiplier): + # type: (float) -> float + for lvl, cpm in cls.STATIC_DATA.iteritems(): + diff = abs(cpm - cp_multiplier) + if diff <= cls.HALF_DIFF_BETWEEN_HALF_LVL: + return float(lvl) + raise ValueError("Unknown cp_multiplier: {}".format(cp_multiplier)) + + +class _Attacks(_StaticInventoryComponent): + BY_NAME = {} # type: Dict[string, Attack] + BY_TYPE = {} # type: Dict[List[Attack]] + BY_DPS = [] # type: List[Attack] + + @classmethod + def process_static_data(cls, moves): + ret = {} + by_type = {} + by_name = {} + fast = cls is FastAttacks + for attack in moves: + attack = Attack(attack) if fast else ChargedAttack(attack) + ret[attack.id] = attack + by_name[attack.name] = attack + + if attack.type not in by_type: + by_type[attack.type] = [] + by_type[attack.type].append(attack) + + for t in by_type.iterkeys(): + attacks = sorted(by_type[t], key=lambda m: m.dps, reverse=True) + min_dps = attacks[-1].dps + max_dps = attacks[0].dps - min_dps + if max_dps > .0: + for attack in attacks: # type: Attack + attack.rate_in_type = (attack.dps - min_dps) / max_dps + by_type[t] = attacks + + cls.BY_NAME = by_name + cls.BY_TYPE = by_type + cls.BY_DPS = sorted(ret.values(), key=lambda m: m.dps, reverse=True) + + return ret + + @classmethod + def data_for(cls, attack_id): + # type: (int) -> Attack + if attack_id not in cls.STATIC_DATA: + raise ValueError("Attack {} not found in {}".format( + attack_id, cls.__name__)) + return cls.STATIC_DATA[attack_id] + + @classmethod + def by_name(cls, name): + # type: (string) -> Attack + return cls.BY_NAME[name] + + @classmethod + def list_for_type(cls, type_name): + # type: (string) -> List[Attack] + """ + :return: Attacks sorted by DPS in descending order + """ + return cls.BY_TYPE[type_name] + + @classmethod + def all(cls): + return cls.STATIC_DATA.values() + + @classmethod + def all_by_dps(cls): + return cls.BY_DPS + + +class FastAttacks(_Attacks): + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'fast_moves.json') + + +class ChargedAttacks(_Attacks): + STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'charged_moves.json') + + +# +# Instances + +class Candy(object): + def __init__(self, family_id, quantity): + self.type = Pokemons.name_for(family_id) + self.quantity = quantity + + def consume(self, amount): + if self.quantity < amount: + raise Exception('Tried to consume more {} candy than you have'.format(self.type)) + self.quantity -= amount + + def add(self, amount): + if amount < 0: + raise Exception('Must add positive amount of candy') + self.quantity += amount + + class Egg(object): def __init__(self, data): self._data = data @@ -157,50 +439,306 @@ def has_next_evolution(self): class Pokemon(object): def __init__(self, data): self._data = data + # Unique ID for this particular Pokemon self.id = data['id'] + # Id of the such pokemons in pokedex self.pokemon_id = data['pokemon_id'] + + # Combat points value self.cp = data['cp'] + # Base CP multiplier, fixed at the catch time + self.cp_bm = data['cp_multiplier'] + # Changeable part of the CP multiplier, increasing at power up + self.cp_am = data.get('additional_cp_multiplier', .0) + # Resulting CP multiplier + self.cp_m = self.cp_bm + self.cp_am + + # Current pokemon level (half of level is a normal value) + self.level = LevelToCPm.level_from_cpm(self.cp_m) + + # Maximum health points + self.hp_max = data['stamina_max'] + # Current health points + self.hp = data.get('stamina', self.hp_max) + assert 0 <= self.hp <= self.hp_max + + # Individial Values of the current pokemon (different for each pokemon) + self.iv_attack = data.get('individual_attack', 0) + self.iv_defense = data.get('individual_defense', 0) + self.iv_stamina = data.get('individual_stamina', 0) + self._static_data = Pokemons.data_for(self.pokemon_id) self.name = Pokemons.name_for(self.pokemon_id) - self.iv = self._compute_iv() + self.nickname = data.get('nickname', self.name) + + self.in_fort = 'deployed_fort_id' in data + self.is_favorite = data.get('favorite', 0) is 1 + + # Basic Values of the current pokemon (identical for all such pokemons) + self.base_attack = self._static_data['BaseAttack'] + self.base_defense = self._static_data['BaseDefense'] + self.base_stamina = self._static_data['BaseStamina'] + + # Maximum possible CP for the current pokemon + self.max_cp = self._static_data['max_cp'] + + self.fast_attack = FastAttacks.data_for(data['move_1']) + self.charged_attack = ChargedAttacks.data_for(data['move_2']) # type: ChargedAttack + + # Internal values (IV) perfection percent + self.iv = self._compute_iv_perfection() + + # IV CP perfection - kind of IV perfection percent but calculated + # using weight of each IV in its contribution to CP of the best + # evolution of current pokemon + # So it tends to be more accurate than simple IV perfection + self.ivcp = self._compute_cp_perfection() + + # Exact value of current CP (not rounded) + self.cp_exact = _calc_cp( + self.base_attack, self.base_defense, self.base_stamina, + self.iv_attack, self.iv_defense, self.iv_stamina, self.cp_m) + assert max(int(self.cp_exact), 10) == self.cp + + # Percent of maximum possible CP + self.cp_percent = self.cp_exact / self.max_cp + + # Get moveset instance with calculated DPS and perfection percents + self.moveset = self._get_moveset() + + def __str__(self): + return self.name + + def __repr__(self): + return self.name def can_evolve_now(self): - return self.has_next_evolution() and self.candy_quantity > self.evolution_cost + return self.has_next_evolution() and \ + self.candy_quantity >= self.evolution_cost def has_next_evolution(self): - return 'Next Evolution Requirements' in self._static_data + return Pokemons.has_next_evolution(self.pokemon_id) def has_seen_next_evolution(self): - return pokedex().captured(self.next_evolution_id) + for pokemon_id in self.next_evolution_ids: + if pokedex().captured(pokemon_id): + return True + return False @property - def next_evolution_id(self): - return Pokemons.next_evolution_id_for(self.pokemon_id) + def family_id(self): + return self.first_evolution_id @property def first_evolution_id(self): return Pokemons.first_evolution_id_for(self.pokemon_id) + @property + def prev_evolution_id(self): + return Pokemons.prev_evolution_id_for(self.pokemon_id) + + @property + def next_evolution_ids(self): + return Pokemons.next_evolution_ids_for(self.pokemon_id) + + @property + def last_evolution_ids(self): + return Pokemons.last_evolution_ids_for(self.pokemon_id) + @property def candy_quantity(self): return candies().get(self.pokemon_id).quantity @property def evolution_cost(self): - return self._static_data['Next Evolution Requirements']['Amount'] + return Pokemons.evolution_cost_for(self.pokemon_id) + + def _compute_iv_perfection(self): + total_iv = self.iv_attack + self.iv_defense + self.iv_stamina + iv_perfection = round((total_iv / 45.0), 2) + return iv_perfection + + def _compute_cp_perfection(self): + """ + CP perfect percent is more accurate than IV perfect + + We know attack plays an important role in CP, and different + pokemons have different base value, that's means 15/14/15 is + better than 14/15/15 for lot of pokemons, and if one pokemon's + base def is more than base sta, 15/15/14 is better than 15/14/15. + + See https://github.com/jabbink/PokemonGoBot/issues/469 + + So calculate CP perfection at final level for the best of the final + evolutions of the pokemon. + """ + variants = [] + iv_attack = self.iv_attack + iv_defense = self.iv_defense + iv_stamina = self.iv_stamina + cp_m = LevelToCPm.MAX_CPM + last_evolution_ids = self.last_evolution_ids + for pokemon_id in last_evolution_ids: + poke_info = Pokemons.data_for(pokemon_id) + base_attack = poke_info['BaseAttack'] + base_defense = poke_info['BaseDefense'] + base_stamina = poke_info['BaseStamina'] + + # calculate CP variants at maximum level + worst_cp = _calc_cp(base_attack, base_defense, base_stamina, + 0, 0, 0, cp_m) + perfect_cp = _calc_cp(base_attack, base_defense, base_stamina, + cp_multiplier=cp_m) + current_cp = _calc_cp(base_attack, base_defense, base_stamina, + iv_attack, iv_defense, iv_stamina, cp_m) + cp_perfection = (current_cp - worst_cp) / (perfect_cp - worst_cp) + variants.append(cp_perfection) + + # get best value (probably for the best evolution) + cp_perfection = max(variants) + return cp_perfection + + def _get_moveset(self): + move1 = self.fast_attack + move2 = self.charged_attack + movesets = self._static_data['movesets'] + current_moveset = None + for moveset in movesets: # type: Moveset + if moveset.fast_attack == move1 and moveset.charged_attack == move2: + current_moveset = moveset + break + + if current_moveset is None: + error = "Unexpected moveset [{}, {}] for #{} {}," \ + " please update info in pokemon.json and create issue/PR"\ + .format(move1, move2, self.pokemon_id, self.name) + # raise ValueError(error) + logging.getLogger(type(self).__name__).error(error) + current_moveset = Moveset( + move1, move2, self._static_data['types'], self.pokemon_id) + + return current_moveset + + +class Attack(object): + def __init__(self, data): + # self._data = data # Not needed - all saved in fields + self.id = data['id'] + self.name = data['name'] + self.type = data['type'] + self.damage = data['damage'] + self.duration = data['duration'] / 1000.0 # duration in seconds + + # Energy addition for fast attack + # Energy cost for charged attack + self.energy = data['energy'] + + # Damage Per Second + # recalc for better precision + self.dps = self.damage / self.duration - def _compute_iv(self): - total_IV = 0.0 - iv_stats = ['individual_attack', 'individual_defense', 'individual_stamina'] + # Perfection of the attack in it's type (from 0 to 1) + self.rate_in_type = .0 - for individual_stat in iv_stats: - try: - total_IV += self._data[individual_stat] - except Exception: - self._data[individual_stat] = 0 - continue - pokemon_potential = round((total_IV / 45.0), 2) - return pokemon_potential + @property + def damage_with_stab(self): + # damage with STAB (Same-type attack bonus) + return self.damage * STAB_FACTOR + + @property + def dps_with_stab(self): + # DPS with STAB (Same-type attack bonus) + return self.dps * STAB_FACTOR + + @property + def energy_per_second(self): + return self.energy / self.duration + + @property + def dodge_window(self): + # TODO: Attack Dodge Window + return NotImplemented + + @property + def is_charged(self): + return False + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + + +class ChargedAttack(Attack): + def __init__(self, data): + super(ChargedAttack, self).__init__(data) + + @property + def is_charged(self): + return True + + +class Moveset(object): + def __init__(self, fm, chm, pokemon_types=(), pokemon_id=-1): + # type: (Attack, ChargedAttack, List[string], int) -> None + if len(pokemon_types) <= 0 < pokemon_id: + pokemon_types = Pokemons.data_for(pokemon_id)['types'] + + self.pokemon_id = pokemon_id + self.fast_attack = fm + self.charged_attack = chm + + # See Pokemons._process_movesets() + # See http://pokemongo.gamepress.gg/optimal-moveset-explanation + # See http://pokemongo.gamepress.gg/defensive-tactics + + fm_number = 100 # for simplicity we use 100 + + fm_energy = fm.energy * fm_number + fm_damage = fm.damage * fm_number + fm_secs = fm.duration * fm_number + + # Defender attacks in intervals of 1 second for the + # first 2 attacks, and then in intervals of 2 seconds + # So add 1.95 seconds to the quick move cool down for defense + # 1.95 is something like an average here + # TODO: Do something better? + fm_defense_secs = (fm.duration + 1.95) * fm_number + + chm_number = fm_energy / chm.energy + chm_damage = chm.damage * chm_number + chm_secs = chm.duration * chm_number + + damage_sum = fm_damage + chm_damage + # raw Damage-Per-Second for the moveset + self.dps = damage_sum / (fm_secs + chm_secs) + # average DPS for defense + self.dps_defense = damage_sum / (fm_defense_secs + chm_secs) + + # apply STAB (Same-type attack bonus) + if fm.type in pokemon_types: + fm_damage *= STAB_FACTOR + if chm.type in pokemon_types: + chm_damage *= STAB_FACTOR + + # DPS for attack (counting STAB) + self.dps_attack = (fm_damage + chm_damage) / (fm_secs + chm_secs) + + # Moveset perfection percent attack and for defense + # Calculated for current pokemon, not between all pokemons + # So 100% perfect moveset can be weak if pokemon is weak (e.g. Caterpie) + self.attack_perfection = .0 + self.defense_perfection = .0 + + # TODO: True DPS for real combat (floor(Attack/200 * MovePower * STAB) + 1) + # See http://pokemongo.gamepress.gg/pokemon-attack-explanation + + def __str__(self): + return '[{}, {}]'.format(self.fast_attack, self.charged_attack) + + def __repr__(self): + return '[{}, {}]'.format(self.fast_attack, self.charged_attack) class Inventory(object): @@ -216,13 +754,55 @@ def refresh(self): # TODO: it would be better if this class was used for all # inventory management. For now, I'm just clearing the old inventory field self.bot.latest_inventory = None - inventory = self.bot.get_inventory()['responses']['GET_INVENTORY'][ - 'inventory_delta']['inventory_items'] + inventory = self.bot.get_inventory()['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] for i in (self.pokedex, self.candy, self.items, self.pokemons): i.refresh(inventory) + user_web_inventory = os.path.join(_base_dir, 'web', 'inventory-%s.json' % (self.bot.config.username)) + with open(user_web_inventory, 'w') as outfile: + json.dump(inventory, outfile) + +# +# Usage helpers + +# STAB (Same-type attack bonus) +STAB_FACTOR = 1.25 _inventory = None +LevelToCPm() # init LevelToCPm +FastAttacks() # init FastAttacks +ChargedAttacks() # init ChargedAttacks + + +def _calc_cp(base_attack, base_defense, base_stamina, + iv_attack=15, iv_defense=15, iv_stamina=15, + cp_multiplier=LevelToCPm.MAX_CPM): + """ + CP calculation + + CP = (Attack * Defense^0.5 * Stamina^0.5 * CP_Multiplier^2) / 10 + CP = (BaseAtk+AtkIV) * (BaseDef+DefIV)^0.5 * (BaseStam+StamIV)^0.5 * Lvl(CPScalar)^2 / 10 + + See https://www.reddit.com/r/TheSilphRoad/comments/4t7r4d/exact_pokemon_cp_formula/ + See https://www.reddit.com/r/pokemongodev/comments/4t7xb4/exact_cp_formula_from_stats_and_cpm_and_an_update/ + See http://pokemongo.gamepress.gg/pokemon-stats-advanced + See http://pokemongo.gamepress.gg/cp-multiplier + See http://gaming.stackexchange.com/questions/280491/formula-to-calculate-pokemon-go-cp-and-hp + + :param base_attack: Pokemon BaseAttack + :param base_defense: Pokemon BaseDefense + :param base_stamina: Pokemon BaseStamina + :param iv_attack: Pokemon IndividualAttack (0..15) + :param iv_defense: Pokemon IndividualDefense (0..15) + :param iv_stamina: Pokemon IndividualStamina (0..15) + :param cp_multiplier: CP Multiplier (0.79030001 is max - value for level 40) + :return: CP as float + """ + return (base_attack + iv_attack) \ + * ((base_defense + iv_defense)**0.5) \ + * ((base_stamina + iv_stamina)**0.5) \ + * (cp_multiplier ** 2) / 10 + def init_inventory(bot): global _inventory @@ -243,9 +823,23 @@ def candies(refresh=False): return _inventory.candy -def pokemons(): +def pokemons(refresh=False): + if refresh: + refresh_inventory() return _inventory.pokemons def items(): return _inventory.items + + +def levels_to_cpm(): + return LevelToCPm + + +def fast_attacks(): + return FastAttacks + + +def charged_attacks(): + return ChargedAttacks diff --git a/pokemongo_bot/socketio_server/app.py b/pokemongo_bot/socketio_server/app.py index 09c237f910..a970a30479 100644 --- a/pokemongo_bot/socketio_server/app.py +++ b/pokemongo_bot/socketio_server/app.py @@ -20,13 +20,13 @@ def remote_control(sid, command): @sio.on('bot:send_reply') def request_reply(sid, response): event = response.pop('command') - account = response.pop('account') + account = response['account'] event = "{}:{}".format(event, account) sio.emit(event, response) @sio.on('bot:broadcast') def bot_broadcast(sid, env): - event = env.pop('event') - account = env.pop('account') + event = env['event'] + account = env['account'] event_name = "{}:{}".format(event, account) - sio.emit(event_name, data=env['data']) + sio.emit(event_name, data=env) diff --git a/pokemongo_bot/step_walker.py b/pokemongo_bot/step_walker.py index f6c2cbfe96..7727dc6a0c 100644 --- a/pokemongo_bot/step_walker.py +++ b/pokemongo_bot/step_walker.py @@ -55,6 +55,17 @@ def step(self): cLng = self.initLng + scaledDLng + random_lat_long_delta() self.api.set_position(cLat, cLng, 0) + self.bot.event_manager.emit( + 'position_update', + sender=self, + level='debug', + data={ + 'current_position': (cLat, cLng), + 'last_position': (self.initLat, self.initLng), + 'distance': '', + 'distance_unit': '' + } + ) self.bot.heartbeat() sleep(1) # sleep one second plus a random delta diff --git a/pokemongo_bot/tree_config_builder.py b/pokemongo_bot/tree_config_builder.py index 6242747b25..57dc9da33c 100644 --- a/pokemongo_bot/tree_config_builder.py +++ b/pokemongo_bot/tree_config_builder.py @@ -61,7 +61,8 @@ def build(self): ) instance = worker(self.bot, task_config) - workers.append(instance) + if instance.enabled: + workers.append(instance) return workers diff --git a/pokemongo_bot/websocket_remote_control.py b/pokemongo_bot/websocket_remote_control.py index c4e15362b6..cd5bd5af96 100644 --- a/pokemongo_bot/websocket_remote_control.py +++ b/pokemongo_bot/websocket_remote_control.py @@ -42,11 +42,16 @@ def on_remote_command(self, command): command_handler() def get_player_info(self): - player_info = self.bot.get_inventory()['responses']['GET_INVENTORY'] + request = self.bot.api.create_request() + request.get_player() + request.get_inventory() + response_dict = request.call() + inventory = response_dict['responses'].get('GET_INVENTORY', {}) + player_info = response_dict['responses'].get('GET_PLAYER', {}) self.sio.emit( 'bot:send_reply', { - 'result': player_info, + 'result': {'inventory': inventory, 'player': player_info}, 'command': 'get_player_info', 'account': self.bot.config.username } diff --git a/run.bat b/run.bat new file mode 100644 index 0000000000..047c8f6fd4 --- /dev/null +++ b/run.bat @@ -0,0 +1,10 @@ +@echo off +set /a x=0 +:LOOP +echo Running pokecli.py for count: %x% +REM Change the path for python.exe if it's different for you +C:\Python27\python.exe pokecli.py +REM Waits for 60 seconds +ping 127.0.0.1 -n 60 > nul +set /a x+=1 +goto :LOOP diff --git a/run.sh b/run.sh index 0812dfac8c..9938f8c5e0 100755 --- a/run.sh +++ b/run.sh @@ -1,21 +1,22 @@ #!/usr/bin/env bash -pokebotpath=$(pwd) +pokebotpath=$(cd "$(dirname "$0")"; pwd) filename="" if [ ! -z $1 ]; then filename=$1 else filename="./configs/config.json" -if [ ! -f "$filename" ] -then -echo "There's no "$filename" file. use setup.sh -config to creat one." fi + +if [ ! -f "$filename" ]; then +echo "There's no "$filename" file. use setup.sh -config to creat one." fi while true do cd $pokebotpath python pokecli.py -cf $filename -read -p "Press any button or wait 20 seconds." -r -s -n1 -t 20 -echo `date`"Pokebot"$*" Stopped." +echo `date`" Pokebot "$*" Stopped." +read -p "Press any button or wait 20 seconds to continue. +" -r -s -n1 -t 20 done exit 0 diff --git a/setup.sh b/setup.sh index bcff0feefb..5fce474b21 100755 --- a/setup.sh +++ b/setup.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -pokebotpath=$(pwd) -backuppath=$(pwd)"/backup" +pokebotpath=$(cd "$(dirname "$0")"; pwd) +backuppath=$pokebotpath"/backup" function Pokebotupdate () { cd $pokebotpath @@ -26,7 +26,7 @@ rm -rf pgoencrypt function Pokebotconfig () { cd $pokebotpath -read -p "1.google 2.ptc +read -p "enter 1 for google or 2 for ptc " auth read -p "Input username " username @@ -37,7 +37,7 @@ Input location " location read -p "Input gmapkey " gmapkey -cp configs/config.json.example configs/config.json +cp -f configs/config.json.example configs/config.json && chmod 755 if [ "$auth" = "2" ] then sed -i "s/google/ptc/g" configs/config.json @@ -46,7 +46,7 @@ sed -i "s/YOUR_USERNAME/$username/g" configs/config.json sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json sed -i "s/SOME_LOCATION/$location/g" configs/config.json sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json -echo "Edit configs/config.json to modify any other config." +echo "Edit ./configs/config.json to modify any other config." } function Pokebotinstall () { @@ -67,12 +67,14 @@ echo "You are on Mac os" sudo brew update sudo brew install --devel protobuf else -echo "Nothing happend." +echo "Please check if you have python pip protobuf gcc make installed on your device." +echo "Wait 5 seconds to continue or Use ctrl+c to interrupt this shell." +sleep 5 fi sudo pip install virtualenv Pokebotupdate Pokebotencrypt -echo "Install complete." +echo "Install complete. Starting to generate config.json." Pokebotconfig } @@ -89,7 +91,7 @@ echo " -i,--install. Install PokemonGo-Bot." echo " -b,--backup. Backup config files." echo " -c,--config. Easy config generator." echo " -e,--encrypt. Make encrypt.so." -echo " -r,--reset. Force sync dev branch." +echo " -r,--reset. Force sync dev branch." echo " -u,--update. Command git pull to update." } @@ -120,12 +122,13 @@ Pokebothelp ;; *.json) filename=$* +echo "It's better to use run.sh, not this one." cd $pokebotpath if [ ! -f ./configs/"$filename" ] then -echo "There's no ./configs/"$filename" file. It's better to use run.sh not this one." +echo "There's no ./configs/"$filename" file. It's better to use run.sh, not this one." else -Pokebotrun +./run.sh ./configs/"$filename" fi ;; *) diff --git a/tests/inventory_test.py b/tests/inventory_test.py new file mode 100644 index 0000000000..3d5ffd66b6 --- /dev/null +++ b/tests/inventory_test.py @@ -0,0 +1,183 @@ +import unittest + +from pokemongo_bot.inventory import * + + +class InventoryTest(unittest.TestCase): + def test_pokemons(self): + # Init data + self.assertEqual(len(Pokemons().all()), 0) # No inventory loaded here + + obj = Pokemons + self.assertEqual(len(obj.STATIC_DATA), 151) + + for poke_info in obj.STATIC_DATA: + name = poke_info['Name'] + pokemon_id = int(poke_info['Number']) + self.assertTrue(1 <= pokemon_id <= 151) + + self.assertGreaterEqual(len(poke_info['movesets']), 1) + self.assertTrue(262 <= poke_info['max_cp'] <= 4145) + self.assertTrue(1 <= len(poke_info['types']) <= 2) + self.assertTrue(40 <= poke_info['BaseAttack'] <= 284) + self.assertTrue(54 <= poke_info['BaseDefense'] <= 242) + self.assertTrue(20 <= poke_info['BaseStamina'] <= 500) + self.assertTrue(.0 <= poke_info['CaptureRate'] <= .56) + self.assertTrue(.0 <= poke_info['FleeRate'] <= .99) + self.assertTrue(1 <= len(poke_info['Weaknesses']) <= 7) + self.assertTrue(3 <= len(name) <= 10) + + self.assertGreaterEqual(len(poke_info['Classification']), 11) + self.assertGreaterEqual(len(poke_info['Fast Attack(s)']), 1) + self.assertGreaterEqual(len(poke_info['Special Attack(s)']), 1) + + self.assertIs(obj.data_for(pokemon_id), poke_info) + self.assertIs(obj.name_for(pokemon_id), name) + + first_evolution_id = obj.first_evolution_id_for(pokemon_id) + self.assertGreaterEqual(first_evolution_id, 1) + next_evolution_ids = obj.next_evolution_ids_for(pokemon_id) + last_evolution_ids = obj.last_evolution_ids_for(pokemon_id) + candies_cost = obj.evolution_cost_for(pokemon_id) + obj.prev_evolution_id_for(pokemon_id) # just call test + self.assertGreaterEqual(len(last_evolution_ids), 1) + + if not obj.has_next_evolution(pokemon_id): + assert 'Next evolution(s)' not in poke_info + assert 'Next Evolution Requirements' not in poke_info + else: + self.assertGreaterEqual(len(next_evolution_ids), 1) + self.assertLessEqual(len(next_evolution_ids), len(last_evolution_ids)) + + reqs = poke_info['Next Evolution Requirements'] + self.assertEqual(reqs["Family"], first_evolution_id) + candies_name = obj.name_for(first_evolution_id) + ' candies' + self.assertEqual(reqs["Name"], candies_name) + self.assertIsNotNone(candies_cost) + self.assertTrue(12 <= candies_cost <= 400) + self.assertEqual(reqs["Amount"], candies_cost) + + evolutions = poke_info["Next evolution(s)"] + self.assertGreaterEqual(len(evolutions), len(next_evolution_ids)) + + for p in evolutions: + p_id = int(p["Number"]) + self.assertNotEqual(p_id, pokemon_id) + self.assertEqual(p["Name"], obj.name_for(p_id)) + + for p_id in next_evolution_ids: + self.assertEqual(obj.prev_evolution_id_for(p_id), pokemon_id) + prev_evs = obj.data_for(p_id)["Previous evolution(s)"] + self.assertGreaterEqual(len(prev_evs), 1) + self.assertEqual(int(prev_evs[-1]["Number"]), pokemon_id) + self.assertEqual(prev_evs[-1]["Name"], name) + + # Only Eevee has 3 next evolutions + self.assertEqual(len(next_evolution_ids), + 1 if pokemon_id != 133 else 3) + + if "Previous evolution(s)" in poke_info: + for p in poke_info["Previous evolution(s)"]: + p_id = int(p["Number"]) + self.assertNotEqual(p_id, pokemon_id) + self.assertEqual(p["Name"], obj.name_for(p_id)) + + # + # Specific pokemons testing + + poke = Pokemon({ + "num_upgrades": 2, "move_1": 210, "move_2": 69, "pokeball": 2, + "favorite": 1, "pokemon_id": 42, "battles_attacked": 4, + "stamina": 76, "stamina_max": 76, "individual_attack": 9, + "individual_defense": 4, "individual_stamina": 8, + "cp_multiplier": 0.4627983868122101, + "additional_cp_multiplier": 0.018886566162109375, + "cp": 653, "nickname": "Golb", "id": 13632861873471324}) + self.assertEqual(poke.level, 12.5) + self.assertEqual(poke.iv, 0.47) + self.assertAlmostEqual(poke.ivcp, 0.488747515) + self.assertAlmostEqual(poke.max_cp, 1921.34561459) + self.assertAlmostEqual(poke.cp_percent, 0.340368964) + self.assertTrue(poke.is_favorite) + self.assertEqual(poke.name, 'Golbat') + self.assertEqual(poke.nickname, "Golb") + self.assertAlmostEqual(poke.moveset.dps, 10.7540173053) + self.assertAlmostEqual(poke.moveset.dps_attack, 12.14462299) + self.assertAlmostEqual(poke.moveset.dps_defense, 4.876681614) + self.assertAlmostEqual(poke.moveset.attack_perfection, 0.4720730048) + self.assertAlmostEqual(poke.moveset.defense_perfection, 0.8158081497) + + poke = Pokemon({ + "move_1": 221, "move_2": 129, "pokemon_id": 19, "cp": 106, + "individual_attack": 6, "stamina_max": 22, "individual_defense": 14, + "cp_multiplier": 0.37523558735847473, "id": 7841053399}) + self.assertEqual(poke.level, 7.5) + self.assertEqual(poke.iv, 0.44) + self.assertAlmostEqual(poke.ivcp, 0.3804059) + self.assertAlmostEqual(poke.max_cp, 581.64643575) + self.assertAlmostEqual(poke.cp_percent, 0.183759867) + self.assertFalse(poke.is_favorite) + self.assertEqual(poke.name, 'Rattata') + self.assertEqual(poke.nickname, 'Rattata') + self.assertAlmostEqual(poke.moveset.dps, 12.5567813108) + self.assertAlmostEqual(poke.moveset.dps_attack, 15.6959766385) + self.assertAlmostEqual(poke.moveset.dps_defense, 5.54282440561) + self.assertAlmostEqual(poke.moveset.attack_perfection, 0.835172881385) + self.assertAlmostEqual(poke.moveset.defense_perfection, 0.603137650999) + + def test_levels_to_cpm(self): + l2c = LevelToCPm + self.assertIs(levels_to_cpm(), l2c) + max_cpm = l2c.cp_multiplier_for(l2c.MAX_LEVEL) + self.assertEqual(l2c.MAX_LEVEL, 40) + self.assertEqual(l2c.MAX_CPM, max_cpm) + self.assertEqual(len(l2c.STATIC_DATA), 79) + + self.assertEqual(l2c.cp_multiplier_for("1"), 0.094) + self.assertEqual(l2c.cp_multiplier_for(1), 0.094) + self.assertEqual(l2c.cp_multiplier_for(1.0), 0.094) + self.assertEqual(l2c.cp_multiplier_for("17.5"), 0.558830576) + self.assertEqual(l2c.cp_multiplier_for(17.5), 0.558830576) + self.assertEqual(l2c.cp_multiplier_for('40.0'), 0.79030001) + self.assertEqual(l2c.cp_multiplier_for(40.0), 0.79030001) + self.assertEqual(l2c.cp_multiplier_for(40), 0.79030001) + + self.assertEqual(l2c.level_from_cpm(0.79030001), 40.0) + self.assertEqual(l2c.level_from_cpm(0.7903), 40.0) + + def test_attacks(self): + self._test_attacks(fast_attacks, FastAttacks) + self._test_attacks(charged_attacks, ChargedAttacks) + + def _test_attacks(self, callback, clazz): + charged = clazz is ChargedAttacks + self.assertIs(callback(), clazz) + + # check consistency + attacks = clazz.all_by_dps() + number = len(attacks) + self.assertTrue(number > 0) + self.assertGreaterEqual(len(clazz.BY_TYPE), 17) + self.assertEqual(number, len(clazz.all())) + self.assertEqual(number, len(clazz.STATIC_DATA)) + self.assertEqual(number, len(clazz.BY_NAME)) + self.assertEqual(number, sum([len(l) for l in clazz.BY_TYPE.values()])) + + # check data + prev_dps = float("inf") + for attack in attacks: # type: Attack + self.assertGreater(attack.id, 0) + self.assertGreater(len(attack.name), 0) + self.assertGreater(len(attack.type), 0) + self.assertGreaterEqual(attack.damage, 0) + self.assertGreater(attack.duration, .0) + self.assertGreater(attack.energy, 0) + self.assertGreaterEqual(attack.dps, 0) + self.assertTrue(.0 <= attack.rate_in_type <= 1.0) + self.assertLessEqual(attack.dps, prev_dps) + self.assertEqual(attack.is_charged, charged) + self.assertIs(attack, clazz.data_for(attack.id)) + self.assertIs(attack, clazz.by_name(attack.name)) + self.assertTrue(attack in clazz.BY_TYPE[attack.type]) + self.assertIsInstance(attack, ChargedAttack if charged else Attack) + prev_dps = attack.dps diff --git a/tests/tree_config_builder_test.py b/tests/tree_config_builder_test.py index 1992c8187c..f8982cbfad 100644 --- a/tests/tree_config_builder_test.py +++ b/tests/tree_config_builder_test.py @@ -86,6 +86,25 @@ def test_task_with_config(self): tree = builder.build() self.assertTrue(tree[0].config.get('longer_eggs_first', False)) + def test_disabling_task(self): + obj = convert_from_json("""[{ + "type": "HandleSoftBan", + "config": { + "enabled": false + } + }, { + "type": "CatchLuredPokemon", + "config": { + "enabled": true + } + }]""") + + builder = TreeConfigBuilder(self.bot, obj) + tree = builder.build() + + self.assertTrue(len(tree) == 1) + self.assertIsInstance(tree[0], CatchLuredPokemon) + def test_load_plugin_task(self): package_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'resources', 'plugin_fixture') plugin_loader = PluginLoader() From 8674d0dbbd54cb84c07e1889de012db414329b03 Mon Sep 17 00:00:00 2001 From: Genesis Date: Thu, 11 Aug 2016 09:23:52 +0200 Subject: [PATCH 099/143] UpdateTitleStats -> UpdateLiveStats, new stat, refactoring (#3467) * Renamed UpdateTitleStats to UpdateLiveStats * Cleaned worker documentation * Added documentation for terminal_log and terminal_title * Fixed https://github.com/PokemonGoF/PokemonGo-Bot/pull/3312#issuecomment-238672978 * Made some refactoring * Added captures_per_hour stat that shows estimated pokemon captures per hour * Added a captures_per_hour method in metrics.py * Added unit tests for features added in https://github.com/PokemonGoF/PokemonGo-Bot/pull/3312 * Added unit tests for captures_per_hour * Avoid useless overhead when no output configured * Added default config values in documentation * Fixed issue with title updating on Windows * See https://github.com/PokemonGoF/PokemonGo-Bot/pull/3472 --- pokemongo_bot/cell_workers/__init__.py | 2 +- ...te_title_stats.py => update_live_stats.py} | 109 +++++++++--------- pokemongo_bot/metrics.py | 8 ++ ...tats_test.py => update_live_stats_test.py} | 89 +++++++++----- 4 files changed, 126 insertions(+), 82 deletions(-) rename pokemongo_bot/cell_workers/{update_title_stats.py => update_live_stats.py} (81%) rename tests/{update_title_stats_test.py => update_live_stats_test.py} (58%) diff --git a/pokemongo_bot/cell_workers/__init__.py b/pokemongo_bot/cell_workers/__init__.py index 68d181947a..7933425b63 100644 --- a/pokemongo_bot/cell_workers/__init__.py +++ b/pokemongo_bot/cell_workers/__init__.py @@ -17,4 +17,4 @@ from collect_level_up_reward import CollectLevelUpReward from follow_cluster import FollowCluster from sleep_schedule import SleepSchedule -from update_title_stats import UpdateTitleStats +from update_live_stats import UpdateLiveStats diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_live_stats.py similarity index 81% rename from pokemongo_bot/cell_workers/update_title_stats.py rename to pokemongo_bot/cell_workers/update_live_stats.py index bc40ed82e8..e51253edc5 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_live_stats.py @@ -6,27 +6,17 @@ from pokemongo_bot.worker_result import WorkerResult from pokemongo_bot.tree_config_builder import ConfigException -class UpdateTitleStats(BaseTask): + +class UpdateLiveStats(BaseTask): """ - Periodically updates the terminal title to display stats about the bot. + Periodically displays stats about the bot in the terminal and/or in its title. Fetching some stats requires making API calls. If you're concerned about the amount of calls your bot is making, don't enable this worker. Example config : { - "type": "UpdateTitleStats", - "config": { - "min_interval": 10, - "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"], - } - } - - You can set a logging on terminal mode like this: - - Example logging on console (and disabling title change): - { - "type": "UpdateTitleStats", + "type": "UpdateLiveStats", "config": { "min_interval": 10, "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"], @@ -34,6 +24,15 @@ class UpdateTitleStats(BaseTask): "terminal_title": false } } + + min_interval : The minimum interval at which the stats are displayed, + in seconds (defaults to 120 seconds). + The update interval cannot be accurate as workers run synchronously. + stats : An array of stats to display and their display order (implicitly), + see available stats below (defaults to []). + terminal_log : Logs the stats into the terminal (defaults to false). + terminal_title : Displays the stats into the terminal title (defaults to true). + Available stats : - login : The account login (from the credentials). - username : The trainer name (asked at first in-game connection). @@ -48,6 +47,7 @@ class UpdateTitleStats(BaseTask): - stops_visited : The number of visited stops. - pokemon_encountered : The number of encountered pokemon. - pokemon_caught : The number of caught pokemon. + - captures_per_hour : The estimated number of pokemon captured per hour. - pokemon_released : The number of released pokemon. - pokemon_evolved : The number of evolved pokemon. - pokemon_unseen : The number of pokemon never seen before. @@ -56,16 +56,9 @@ class UpdateTitleStats(BaseTask): - stardust_earned : The number of earned stardust since the bot started. - highest_cp_pokemon : The caught pokemon with the highest CP since the bot started. - most_perfect_pokemon : The most perfect caught pokemon since the bot started. - - min_interval : The minimum interval at which the title is updated, - in seconds (defaults to 10 seconds). - The update interval cannot be accurate as workers run synchronously. - stats : An array of stats to display and their display order (implicitly), - see available stats above. """ SUPPORTED_TASK_API_VERSION = 1 - def __init__(self, bot, config): """ Initializes the worker. @@ -74,62 +67,78 @@ def __init__(self, bot, config): :param config: The task configuration. :type config: dict """ - super(UpdateTitleStats, self).__init__(bot, config) + super(UpdateLiveStats, self).__init__(bot, config) self.next_update = None self.min_interval = int(self.config.get('min_interval', 120)) self.displayed_stats = self.config.get('stats', []) - self.terminal_log = self.config.get('terminal_log', False) - self.terminal_title = self.config.get('terminal_title', True) + self.terminal_log = bool(self.config.get('terminal_log', False)) + self.terminal_title = bool(self.config.get('terminal_title', True)) - self.bot.event_manager.register_event('update_title', parameters=('title',)) - self.bot.event_manager.register_event('log_stats',parameters=('title',)) + self.bot.event_manager.register_event('log_stats', parameters=('stats',)) def initialize(self): pass def work(self): """ - Updates the title if necessary. + Displays the stats if necessary. :return: Always returns WorkerResult.SUCCESS. :rtype: WorkerResult """ if not self._should_display(): return WorkerResult.SUCCESS - title = self._get_stats_title(self._get_player_stats()) - # If title is empty, it couldn't be generated. - if not title: + line = self._get_stats_line(self._get_player_stats()) + # If line is empty, it couldn't be generated. + if not line: return WorkerResult.SUCCESS if self.terminal_title: - self._update_title(title, _platform) + self._update_title(line, _platform) if self.terminal_log: - self._log_on_terminal(title) + self._log_on_terminal(line) return WorkerResult.SUCCESS def _should_display(self): """ - Returns a value indicating whether the title should be updated. - :return: True if the title should be updated; otherwise, False. + Returns a value indicating whether the stats should be displayed. + :return: True if the stats should be displayed; otherwise, False. :rtype: bool """ + if not self.terminal_title and not self.terminal_log: + return False return self.next_update is None or datetime.now() >= self.next_update - def _log_on_terminal(self, title): + def _compute_next_update(self): + """ + Computes the next update datetime based on the minimum update interval. + :return: Nothing. + :rtype: None + """ + self.next_update = datetime.now() + timedelta(seconds=self.min_interval) + + def _log_on_terminal(self, stats): + """ + Logs the stats into the terminal using an event. + :param stats: The stats to display. + :type stats: string + :return: Nothing. + :rtype: None + """ self.emit_event( 'log_stats', - formatted="{title}", + formatted="{stats}", data={ - 'title': title + 'stats': stats } ) - self.next_update = datetime.now() + timedelta(seconds=self.min_interval) + self._compute_next_update() def _update_title(self, title, platform): """ - Updates the window title using different methods, according to the given platform + Updates the window title using different methods, according to the given platform. :param title: The new window title. :type title: string :param platform: The platform string. @@ -139,14 +148,6 @@ def _update_title(self, title, platform): :raise: RuntimeError: When the given platform isn't supported. """ - self.emit_event( - 'update_title', - formatted="{title}", - data={ - 'title': title - } - ) - if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) stdout.flush() @@ -154,14 +155,12 @@ def _update_title(self, title, platform): stdout.write("\033]0;{}\007".format(title)) stdout.flush() elif platform == "win32": - ctypes.windll.kernel32.SetConsoleTitleA(title) + ctypes.windll.kernel32.SetConsoleTitleA(title.encode()) else: raise RuntimeError("unsupported platform '{}'".format(platform)) + self._compute_next_update() - self.next_update = datetime.now() + timedelta(seconds=self.min_interval) - - - def _get_stats_title(self, player_stats): + def _get_stats_line(self, player_stats): """ Generates a stats string with the given player stats according to the configuration. :return: A string containing human-readable stats, ready to be displayed. @@ -194,6 +193,7 @@ def _get_stats_title(self, player_stats): stops_visited = metrics.visits['latest'] - metrics.visits['start'] pokemon_encountered = metrics.num_encounters() pokemon_caught = metrics.num_captures() + captures_per_hour = int(metrics.captures_per_hour()) pokemon_released = metrics.releases pokemon_evolved = metrics.num_evolutions() pokemon_unseen = metrics.num_new_mons() @@ -223,6 +223,7 @@ def _get_stats_title(self, player_stats): 'stops_visited': 'Visited {:,} stops'.format(stops_visited), 'pokemon_encountered': 'Encountered {:,} pokemon'.format(pokemon_encountered), 'pokemon_caught': 'Caught {:,} pokemon'.format(pokemon_caught), + 'captures_per_hour': '{:,} pokemon/h'.format(captures_per_hour), 'pokemon_released': 'Released {:,} pokemon'.format(pokemon_released), 'pokemon_evolved': 'Evolved {:,} pokemon'.format(pokemon_evolved), 'pokemon_unseen': 'Encountered {} new pokemon'.format(pokemon_unseen), @@ -251,9 +252,9 @@ def get_stat(stat): return available_stats[stat] # Map stats the user wants to see to available stats and join them with pipes. - title = ' | '.join(map(get_stat, self.displayed_stats)) + line = ' | '.join(map(get_stat, self.displayed_stats)) - return title + return line def _get_player_stats(self): """ diff --git a/pokemongo_bot/metrics.py b/pokemongo_bot/metrics.py index 0ffeb39a6c..4a6a70cae3 100644 --- a/pokemongo_bot/metrics.py +++ b/pokemongo_bot/metrics.py @@ -42,6 +42,14 @@ def num_throws(self): def num_captures(self): return self.captures['latest'] - self.captures['start'] + def captures_per_hour(self): + """ + Returns an estimated number of pokemon caught per hour. + :return: An estimated number of pokemon caught per hour. + :rtype: float + """ + return self.num_captures() / (time.time() - self.start_time) * 3600 + def num_visits(self): return self.visits['latest'] - self.visits['start'] diff --git a/tests/update_title_stats_test.py b/tests/update_live_stats_test.py similarity index 58% rename from tests/update_title_stats_test.py rename to tests/update_live_stats_test.py index ba480f0151..dc5b140080 100644 --- a/tests/update_title_stats_test.py +++ b/tests/update_live_stats_test.py @@ -2,18 +2,20 @@ from sys import platform as _platform from datetime import datetime, timedelta from mock import call, patch, MagicMock -from pokemongo_bot.cell_workers.update_title_stats import UpdateTitleStats +from pokemongo_bot.cell_workers.update_live_stats import UpdateLiveStats from tests import FakeBot -class UpdateTitleStatsTestCase(unittest.TestCase): +class UpdateLiveStatsTestCase(unittest.TestCase): config = { 'min_interval': 20, 'stats': ['login', 'username', 'pokemon_evolved', 'pokemon_encountered', 'uptime', 'pokemon_caught', 'stops_visited', 'km_walked', 'level', 'stardust_earned', 'level_completion', 'xp_per_hour', 'pokeballs_thrown', 'highest_cp_pokemon', 'level_stats', 'xp_earned', 'pokemon_unseen', 'most_perfect_pokemon', - 'pokemon_stats', 'pokemon_released'] + 'pokemon_stats', 'pokemon_released', 'captures_per_hour'], + 'terminal_log': True, + 'terminal_title': False } player_stats = { 'level': 25, @@ -26,7 +28,7 @@ def setUp(self): self.bot = FakeBot() self.bot._player = {'username': 'Username'} self.bot.config.username = 'Login' - self.worker = UpdateTitleStats(self.bot, self.config) + self.worker = UpdateLiveStats(self.bot, self.config) def mock_metrics(self): self.bot.metrics = MagicMock() @@ -37,6 +39,7 @@ def mock_metrics(self): self.bot.metrics.visits = {'latest': 250, 'start': 30} self.bot.metrics.num_encounters.return_value = 130 self.bot.metrics.num_captures.return_value = 120 + self.bot.metrics.captures_per_hour.return_value = 75 self.bot.metrics.releases = 30 self.bot.metrics.num_evolutions.return_value = 12 self.bot.metrics.num_new_mons.return_value = 3 @@ -45,16 +48,30 @@ def mock_metrics(self): self.bot.metrics.highest_cp = {'desc': 'highest_cp'} self.bot.metrics.most_perfect = {'desc': 'most_perfect'} - def test_process_config(self): + def test_config(self): self.assertEqual(self.worker.min_interval, self.config['min_interval']) self.assertEqual(self.worker.displayed_stats, self.config['stats']) + self.assertEqual(self.worker.terminal_title, self.config['terminal_title']) + self.assertEqual(self.worker.terminal_log, self.config['terminal_log']) def test_should_display_no_next_update(self): self.worker.next_update = None self.assertTrue(self.worker._should_display()) - @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + @patch('pokemongo_bot.cell_workers.update_live_stats.datetime') + def test_should_display_no_terminal_log_title(self, mock_datetime): + # _should_display should return False if both terminal_title and terminal_log are false + # in configuration, even if we're past next_update. + now = datetime.now() + mock_datetime.now.return_value = now + timedelta(seconds=20) + self.worker.next_update = now + self.worker.terminal_log = False + self.worker.terminal_title = False + + self.assertFalse(self.worker._should_display()) + + @patch('pokemongo_bot.cell_workers.update_live_stats.datetime') def test_should_display_before_next_update(self, mock_datetime): now = datetime.now() mock_datetime.now.return_value = now - timedelta(seconds=20) @@ -62,7 +79,7 @@ def test_should_display_before_next_update(self, mock_datetime): self.assertFalse(self.worker._should_display()) - @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + @patch('pokemongo_bot.cell_workers.update_live_stats.datetime') def test_should_display_after_next_update(self, mock_datetime): now = datetime.now() mock_datetime.now.return_value = now + timedelta(seconds=20) @@ -70,7 +87,7 @@ def test_should_display_after_next_update(self, mock_datetime): self.assertTrue(self.worker._should_display()) - @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + @patch('pokemongo_bot.cell_workers.update_live_stats.datetime') def test_should_display_exactly_next_update(self, mock_datetime): now = datetime.now() mock_datetime.now.return_value = now @@ -78,65 +95,83 @@ def test_should_display_exactly_next_update(self, mock_datetime): self.assertTrue(self.worker._should_display()) - @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') - def test_next_update_after_update_title(self, mock_datetime): + @patch('pokemongo_bot.cell_workers.update_live_stats.datetime') + def test_compute_next_update(self, mock_datetime): now = datetime.now() mock_datetime.now.return_value = now old_next_display_value = self.worker.next_update - self.worker._update_title('', 'linux2') + self.worker._compute_next_update() self.assertNotEqual(self.worker.next_update, old_next_display_value) self.assertEqual(self.worker.next_update, now + timedelta(seconds=self.config['min_interval'])) - @patch('pokemongo_bot.cell_workers.update_title_stats.stdout') - def test_update_title_linux_cygwin(self, mock_stdout): + @patch('pokemongo_bot.cell_workers.update_live_stats.stdout') + @patch('pokemongo_bot.cell_workers.UpdateLiveStats._compute_next_update') + def test_update_title_linux_cygwin(self, mock_compute_next_update, mock_stdout): self.worker._update_title('new title linux', 'linux') self.assertEqual(mock_stdout.write.call_count, 1) self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title linux\x07')) + self.assertEqual(mock_compute_next_update.call_count, 1) self.worker._update_title('new title linux2', 'linux2') self.assertEqual(mock_stdout.write.call_count, 2) self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title linux2\x07')) + self.assertEqual(mock_compute_next_update.call_count, 2) self.worker._update_title('new title cygwin', 'cygwin') self.assertEqual(mock_stdout.write.call_count, 3) self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title cygwin\x07')) + self.assertEqual(mock_compute_next_update.call_count, 3) - @patch('pokemongo_bot.cell_workers.update_title_stats.stdout') - def test_update_title_darwin(self, mock_stdout): + @patch('pokemongo_bot.cell_workers.update_live_stats.stdout') + @patch('pokemongo_bot.cell_workers.UpdateLiveStats._compute_next_update') + def test_update_title_darwin(self, mock_compute_next_update, mock_stdout): self.worker._update_title('new title darwin', 'darwin') self.assertEqual(mock_stdout.write.call_count, 1) self.assertEqual(mock_stdout.write.call_args, call('\033]0;new title darwin\007')) + self.assertEqual(mock_compute_next_update.call_count, 1) @unittest.skipUnless(_platform.startswith("win"), "requires Windows") - @patch('pokemongo_bot.cell_workers.update_title_stats.ctypes') - def test_update_title_win32(self, mock_ctypes): + @patch('pokemongo_bot.cell_workers.update_live_stats.ctypes') + @patch('pokemongo_bot.cell_workers.UpdateLiveStats._compute_next_update') + def test_update_title_win32(self, mock_compute_next_update, mock_ctypes): self.worker._update_title('new title win32', 'win32') self.assertEqual(mock_ctypes.windll.kernel32.SetConsoleTitleA.call_count, 1) self.assertEqual(mock_ctypes.windll.kernel32.SetConsoleTitleA.call_args, call('new title win32')) + self.assertEqual(mock_compute_next_update.call_count, 1) + + @patch('pokemongo_bot.cell_workers.update_live_stats.BaseTask.emit_event') + @patch('pokemongo_bot.cell_workers.UpdateLiveStats._compute_next_update') + def test_log_on_terminal(self, mock_compute_next_update, mock_emit_event): + self.worker._log_on_terminal('stats') + + self.assertEqual(mock_emit_event.call_count, 1) + self.assertEqual(mock_emit_event.call_args, + call('log_stats', data={'stats': 'stats'}, formatted='{stats}')) + self.assertEqual(mock_compute_next_update.call_count, 1) - def test_get_stats_title_player_stats_none(self): - title = self.worker._get_stats_title(None) + def test_get_stats_line_player_stats_none(self): + line = self.worker._get_stats_line(None) - self.assertEqual(title, '') + self.assertEqual(line, '') - def test_get_stats_no_displayed_stats(self): + def test_get_stats_line_no_displayed_stats(self): self.worker.displayed_stats = [] - title = self.worker._get_stats_title(self.player_stats) + line = self.worker._get_stats_line(self.player_stats) - self.assertEqual(title, '') + self.assertEqual(line, '') - def test_get_stats(self): + def test_get_stats_line(self): self.mock_metrics() - title = self.worker._get_stats_title(self.player_stats) + line = self.worker._get_stats_line(self.player_stats) expected = 'Login | Username | Evolved 12 pokemon | Encountered 130 pokemon | ' \ 'Uptime : 15:42:13 | Caught 120 pokemon | Visited 220 stops | ' \ '42.05km walked | Level 25 | Earned 24,069 Stardust | ' \ @@ -145,6 +180,6 @@ def test_get_stats(self): '+424,242 XP | Encountered 3 new pokemon | ' \ 'Most perfect pokemon : most_perfect | ' \ 'Encountered 130 pokemon, 120 caught, 30 released, 12 evolved, ' \ - '3 never seen before | Released 30 pokemon' + '3 never seen before | Released 30 pokemon | 75 pokemon/h' - self.assertEqual(title, expected) + self.assertEqual(line, expected) From 045f297563fb28eaf17fc047d4d3d438aced2d94 Mon Sep 17 00:00:00 2001 From: nivong Date: Thu, 11 Aug 2016 09:29:10 +0200 Subject: [PATCH 100/143] Update installation docs to reflect setup sh changes (#3567) --- docs/installation.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/installation.md b/docs/installation.md index 8f5f5692e3..6c515dfee2 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -7,6 +7,18 @@ - [docker](https://docs.docker.com/engine/installation/) (Optional) - [how to setup after installation](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/How-to-run-with-Docker) - [protobuf 3](https://github.com/google/protobuf) (OS Dependent, see below) +#Linux/Mac Automatic installation +### Easy installation (automatic, linux only) +1. Run setup.sh -e +2. Run setup.sh -i +3. Run setup.sh -c + +### To update +1. Run setup.sh -r +2. Run setup.sh -u + + +# Manual installation ### Protobuf 3 installation - OS X: `brew update && brew install --devel protobuf` From 350148e8443e35a6f5b74169b7ac5fd26815fe7b Mon Sep 17 00:00:00 2001 From: nivong Date: Thu, 11 Aug 2016 09:29:57 +0200 Subject: [PATCH 101/143] Fixed chmod in setup.sh (#3565) * forgot to include the config location * fixed setup files --- CONTRIBUTORS.md | 1 + setup.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index fc2478f20c..136886b5aa 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -62,3 +62,4 @@ * eevee-github * g0vanish * cmezh + * Nivong diff --git a/setup.sh b/setup.sh index 5fce474b21..81c8d13419 100755 --- a/setup.sh +++ b/setup.sh @@ -37,7 +37,7 @@ Input location " location read -p "Input gmapkey " gmapkey -cp -f configs/config.json.example configs/config.json && chmod 755 +cp -f configs/config.json.example configs/config.json && chmod 755 configs/config.json if [ "$auth" = "2" ] then sed -i "s/google/ptc/g" configs/config.json From de09f34ebb292d303ba44066fde842f293546594 Mon Sep 17 00:00:00 2001 From: nivong Date: Thu, 11 Aug 2016 10:07:44 +0200 Subject: [PATCH 102/143] Updated readme to have better readability (#3569) * Made the READ me more read friendly and compactor. * Update README.md * Update README.md * Update README.md * Update README.md * Update faq.md --- README.md | 32 +++++++------------------------- docs/faq.md | 9 +++++++-- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index d372870ca6..d2172efbb2 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,14 @@ - -# PokemonGo-Bot (Working) +# PokemonGo-Bot PokemonGo bot is a project created by the [PokemonGoF](https://github.com/PokemonGoF) team. -The project is currently setup in two main branches. `dev` and `master`. - -## Help Needed on [Desktop Version](https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) -## Please submit PR to [Dev branch](https://github.com/PokemonGoF/PokemonGo-Bot/tree/dev) +The project is currently setup in two main branches. `dev` also known as `beta` and `master` also known as `stable`. Submit your PR's to `dev`. -We use [Slack](https://slack.com) as a web chat. [Click here to join the chat!](https://pokemongo-bot.herokuapp.com) -You can count on the community in #help channel. +If you need any help please don't create an issue here on github we have a great community on Slack, [Click here to join the chat!](https://pokemongo-bot.herokuapp.com). You can count on the community in #help channel. ## Table of Contents +- [Installation] (https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/installation.md) - [Features](#features) -- [Wiki](#wiki) +- [Wiki](https://github.com/PokemonGoF/PokemonGo-Bot/wiki) - [Credits](#credits) ## Features @@ -27,9 +23,9 @@ You can count on the community in #help channel. - [x] Rudimentary IV Functionality filter - [x] Ignore certain pokemon filter - [x] Adjust delay between Pokemon capture & Transfer as per configuration -- [ ] Standalone Desktop Application - [x] Hatch eggs - [x] Incubate eggs +- [ ] [Standalone Desktop Application] (https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) - [ ] Use candy - [ ] Inventory cleaner @@ -45,22 +41,8 @@ If there are any concerns with this policy or you believe we are tracking someth If you do not want any data to be gathered, you can turn off this feature by setting `health_record` to `false` in your `config.json`. -## Wiki -All information on [Getting Started](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Getting-Started) is available in the [Wiki](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/)! -- __Installation__ - - [Requirements] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#requirements-click-each-one-for-install-guide) - - [How to run with Docker](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/How-to-run-with-Docker) - - [Linux] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-linux) - - [Mac] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-mac) - - [Windows] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Installation#installation-windows) -- [Develop PokemonGo-Bot](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Develop-PokemonGo-Bot) -- [Plugins](https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/plugins.md) -- [Configuration-files](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Configuration-files) -- [Front end web module - Google Maps API] (https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Google-Maps-API-(web-page)) -- [Docker Usage](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ#how-to-run-with-docker) -- [FAQ](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/FAQ) +## Help Needed on [Desktop Version](https://github.com/PokemonGoF/PokemonGo-Bot-Desktop) -To ensure that all updates are documented - [@eggins](https://github.com/eggins) will keep the Wiki updated with the latest information on installing, updating and configuring the bot. ## Credits - [tejado](https://github.com/tejado) many thanks for the API diff --git a/docs/faq.md b/docs/faq.md index 7720f0a64c..ba2524bbce 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,7 +1,12 @@ ### How do I start the application? -After customizing your config.json files, cd to the PokemonGo-Bot folder and enter: +After [installing] (https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/installation.md), in the root folder run the following command: +### Linux ``` -$ python pokecli.py +run.sh +``` +### Windows +``` +run.bat ``` This will start the application. From 962313328db8c6509abef217b6e98dbaf4c3b3fb Mon Sep 17 00:00:00 2001 From: nivong Date: Thu, 11 Aug 2016 10:26:47 +0200 Subject: [PATCH 103/143] More documentation changes making it noob proof(er) (#3575) * added information on what what does * add a link to the docker hub for NAS users. --- docs/docker.md | 10 +++++++++- docs/installation.md | 14 +++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/docs/docker.md b/docs/docker.md index 04823a2f5c..735cdd99ec 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,4 +1,12 @@ -Start by downloading for your platform: [Mac](https://www.docker.com/products/docker#/mac), [Windows](https://www.docker.com/products/docker#/windows), or [Linux](https://www.docker.com/products/docker#/linux). Once you have Docker installed, simply create the various config.json files for your different accounts (e.g. `configs/config-account1.json`) and then create a Docker image for PokemonGo-Bot using the Dockerfile in this repo. +Start by downloading for your platform: [Mac](https://www.docker.com/products/docker#/mac), [Windows](https://www.docker.com/products/docker#/windows), or [Linux](https://www.docker.com/products/docker#/linux). Once you have Docker installed, simply create the various config.json files for your different accounts (e.g. `configs/config-account1.json`) and then create a Docker image for PokemonGo-Bot using the Dockerfile in this [repo](https://hub.docker.com/r/svlentink/pokemongo-bot/). + +#Automatic setup +Use this docker hub url: https://hub.docker.com/r/svlentink/pokemongo-bot/ +``` +docker pull svlentink/pokemongo-bot +``` + +#Manual setup ``` cd PokemonGo-Bot docker build -t pokemongo-bot . diff --git a/docs/installation.md b/docs/installation.md index 6c515dfee2..d469fa85f5 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -8,14 +8,22 @@ - [protobuf 3](https://github.com/google/protobuf) (OS Dependent, see below) #Linux/Mac Automatic installation -### Easy installation (automatic, linux only) +### Easy installation 1. Run setup.sh -e + This will create the needed encrypted.so file 2. Run setup.sh -i + This will install the bot and all stuff that is needed to run it (follow the guide) 3. Run setup.sh -c + This will make the config file needed, only basic stuff is changed here. If you want to edit more edit this file after: config/config.json +4. Run run.sh + This will run the bot and will start leveling your pokemon go account. ### To update -1. Run setup.sh -r -2. Run setup.sh -u +1. Stop the bot if it's running. (use control + c twice to stop it) +2. Run setup.sh -r + This will reset and makes sure you have no changes made to any code since it will overide it +3. Run setup.sh -u + This will run git pull and will update to the new git update. # Manual installation From 59fea6c888a913bef8048da90600f4659c7648d4 Mon Sep 17 00:00:00 2001 From: nivong Date: Thu, 11 Aug 2016 10:31:26 +0200 Subject: [PATCH 104/143] Update Readme to point to the latest wiki and documentation (#3579) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d2172efbb2..1c0fa41910 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ The project is currently setup in two main branches. `dev` also known as `beta` If you need any help please don't create an issue here on github we have a great community on Slack, [Click here to join the chat!](https://pokemongo-bot.herokuapp.com). You can count on the community in #help channel. ## Table of Contents -- [Installation] (https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/installation.md) +- [Installation](https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/installation.md) +- [Documentation](https://github.com/PokemonGoF/PokemonGo-Bot/blob/dev/docs/) - [Features](#features) -- [Wiki](https://github.com/PokemonGoF/PokemonGo-Bot/wiki) - [Credits](#credits) ## Features From d5812e9053de1ac52f6487969e1a616148e4889b Mon Sep 17 00:00:00 2001 From: Yong Wen Chua Date: Thu, 11 Aug 2016 17:10:51 +0800 Subject: [PATCH 105/143] Improve Docker Image and `docker-compose.yml` (#3583) - Use the non-`onbuild` variant of the python base image to better make use of Docker image caching - Update some volumes in `docker-compose.yml` for pogoweb --- Dockerfile | 12 +++++++++--- docker-compose.yml | 9 +++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index f98d5d6942..2520813273 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,7 @@ -FROM python:2.7-onbuild +FROM python:2.7 + +WORKDIR /usr/src/app +VOLUME ["/usr/app/configs", "/usr/src/app/web"] ARG timezone=Etc/UTC RUN echo $timezone > /etc/timezone \ @@ -15,8 +18,11 @@ RUN cd /tmp && wget "http://pgoapi.com/pgoencrypt.tar.gz" \ && cd /tmp \ && rm -rf /tmp/pgoencrypt* -VOLUME ["/usr/src/app/web"] - ENV LD_LIBRARY_PATH /usr/src/app +COPY requirements.txt /usr/src/app/ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . /usr/src/app + ENTRYPOINT ["python", "pokecli.py"] diff --git a/docker-compose.yml b/docker-compose.yml index 95a32f8ceb..d31830d401 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,8 @@ services: bot1-pokego: build: . volumes: - - ./configs/config.json:/usr/src/app/configs/config.json + - ./configs:/usr/src/app/configs + - ./web:/usr/src/app/web stdin_open: true tty: true bot1-pokegoweb: @@ -11,10 +12,10 @@ services: ports: - "8000:8000" volumes_from: - - bot1-pokego + - bot1-pokego volumes: - - ./configs/userdata.js:/usr/src/app/web/config/userdata.js + - ./configs:/usr/src/app/web/config working_dir: /usr/src/app/web command: bash -c "echo 'Serving HTTP on 0.0.0.0 port 8000' && python -m SimpleHTTPServer > /dev/null 2>&1" depends_on: - - bot1-pokego \ No newline at end of file + - bot1-pokego From 651c909c697784ef7344eea997d78613d54d141f Mon Sep 17 00:00:00 2001 From: Konstantin Shapkin Date: Thu, 11 Aug 2016 12:53:57 +0300 Subject: [PATCH 106/143] Small fixes and improvments in setup.sh (#3585) Adding -p to mkdir - it will not show "already exist folder" error. And adding to backup *gpx and path files. --- CONTRIBUTORS.md | 1 + setup.sh | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 136886b5aa..7374cfa94d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -63,3 +63,4 @@ * g0vanish * cmezh * Nivong + * kestel diff --git a/setup.sh b/setup.sh index 81c8d13419..c5acdf9c8b 100755 --- a/setup.sh +++ b/setup.sh @@ -109,8 +109,10 @@ Pokebotreset Pokebotupdate ;; --backup|-b) -mkdir $backuppath +mkdir -p $backuppath cp -f $pokebotpath/configs/config*.json $backuppath/ +cp -f $pokebotpath/configs/*.gpx $backuppath/ +cp -f $pokebotpath/configs/path*.json $backuppath/ cp -f $pokebotpath/web/config/userdata.js $backuppath/ echo "Backup complete" ;; From 0292c2f426b2432be70b0ca867fa62d8436cb5bf Mon Sep 17 00:00:00 2001 From: achretien Date: Thu, 11 Aug 2016 12:36:05 +0200 Subject: [PATCH 107/143] Remove the "evolve_captured" flag in favor of the EvolveTask (#3530) * Remove the "evolve_captured" flag in favor of the EvolveTask * Remove unused event * Warn the user instead of stopping the bot --- configs/config.json.cluster.example | 1 - configs/config.json.example | 1 - configs/config.json.map.example | 1 - configs/config.json.path.example | 1 - configs/config.json.pokemon.example | 1 - pokecli.py | 20 +------ pokemongo_bot/__init__.py | 4 -- .../cell_workers/pokemon_catch_worker.py | 54 ------------------- .../event_handlers/colored_logging_handler.py | 1 - 9 files changed, 2 insertions(+), 82 deletions(-) diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index eb507cd43c..73ba037703 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -78,7 +78,6 @@ "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, - "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, "logging_color": true, diff --git a/configs/config.json.example b/configs/config.json.example index 59974ba156..1594a18f5e 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -93,7 +93,6 @@ "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, - "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, "logging_color": true, diff --git a/configs/config.json.map.example b/configs/config.json.map.example index cf6604976b..f20fb9f1ac 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -320,7 +320,6 @@ "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, - "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, "logging_color": true, diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 6b6619573b..08867f1967 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -80,7 +80,6 @@ "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, - "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, "logging_color": true, diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 1dfa01199d..3216ad6681 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -86,7 +86,6 @@ "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, - "evolve_captured": "NONE", "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, "logging_color": true, diff --git a/pokecli.py b/pokecli.py index f59ae3acb9..4544ccf124 100644 --- a/pokecli.py +++ b/pokecli.py @@ -326,15 +326,6 @@ def _json_loader(filename): type=str, default='km' ) - add_config( - parser, - load, - short_flag="-ec", - long_flag="--evolve_captured", - help="(Ad-hoc mode) Pass \"all\" or a list of pokemon to evolve (e.g., \"Pidgey,Weedle,Caterpie\"). Bot will attempt to evolve all the pokemon captured!", - type=str, - default=[] - ) add_config( parser, load, @@ -453,12 +444,8 @@ def task_configuration_error(flag_name): task_configuration_error('{}.{}'.format(outer, inner)) return None - if (config.evolve_captured - and (not isinstance(config.evolve_captured, str) - or str(config.evolve_captured).lower() in ["true", "false"])): - parser.error('"evolve_captured" should be list of pokemons: use "all" or "none" to match all ' + - 'or none of the pokemons, or use a comma separated list such as "Pidgey,Weedle,Caterpie"') - return None + if "evolve_captured" in load: + logger.warning('The evolve_captured argument is no longer supported. Please use the EvolvePokemon task instead') if not (config.location or config.location_cache): parser.error("Needs either --use-location-cache or --location.") @@ -483,9 +470,6 @@ def task_configuration_error(flag_name): if not os.path.isdir(web_dir): raise - if config.evolve_captured and isinstance(config.evolve_captured, str): - config.evolve_captured = [str(pokemon_name).strip() for pokemon_name in config.evolve_captured.split(',')] - fix_nested_config(config) return config diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 1b18ac7ff8..5d10cfba02 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -301,10 +301,6 @@ def _register_events(self): 'pokemon_evolved', parameters=('pokemon', 'iv', 'cp') ) - self.event_manager.register_event( - 'pokemon_evolve_fail', - parameters=('pokemon',) - ) self.event_manager.register_event('skip_evolve') self.event_manager.register_event('threw_berry_failed', parameters=('status_code',)) self.event_manager.register_event('vip_pokemon') diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 3b2092e535..77e29ae10b 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -191,30 +191,6 @@ def _is_vip_pokemon(self, pokemon): return True return self._pokemon_matches_config(self.config.vips, pokemon, default_logic='or') - def _get_current_pokemon_ids(self): - # don't use cached bot.get_inventory() here because we need to have actual information in capture logic - response_dict = self.api.get_inventory() - - try: - inventory_items = response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] - except KeyError: - return [] # no items - - id_list = [] - for item in inventory_items: - try: - pokemon = item['inventory_item_data']['pokemon_data'] - except KeyError: - continue - - # ignore eggs - if pokemon.get('is_egg'): - continue - - id_list.append(pokemon['id']) - - return id_list - def _pct(self, rate_by_ball): return '{0:.2f}'.format(rate_by_ball * 100) @@ -326,9 +302,6 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) berry_count -= 1 - # get current pokemon list before catch - pokemon_before_catch = self._get_current_pokemon_ids() - # try to catch pokemon! items_stock[current_ball] -= 1 self.emit_event( @@ -418,31 +391,4 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): self.bot.softban = False - # evolve pokemon if necessary - if self.config.evolve_captured and (self.config.evolve_captured[0] == 'all' or pokemon.name in self.config.evolve_captured): - pokemon_after_catch = self._get_current_pokemon_ids() - pokemon_to_evolve = list(set(pokemon_after_catch) - set(pokemon_before_catch)) - - if len(pokemon_to_evolve) == 0: - break - - self._do_evolve(pokemon, pokemon_to_evolve[0]) - break - - def _do_evolve(self, pokemon, new_pokemon_id): - response_dict = self.api.evolve_pokemon(pokemon_id=new_pokemon_id) - catch_pokemon_status = response_dict['responses']['EVOLVE_POKEMON']['result'] - - if catch_pokemon_status == 1: - self.emit_event( - 'pokemon_evolved', - formatted='{pokemon} evolved!', - data={'pokemon': pokemon.name} - ) - else: - self.emit_event( - 'pokemon_evolve_fail', - formatted='Failed to evolve {pokemon}!', - data={'pokemon': pokemon.name} - ) diff --git a/pokemongo_bot/event_handlers/colored_logging_handler.py b/pokemongo_bot/event_handlers/colored_logging_handler.py index f3c902d464..6c527511ba 100644 --- a/pokemongo_bot/event_handlers/colored_logging_handler.py +++ b/pokemongo_bot/event_handlers/colored_logging_handler.py @@ -71,7 +71,6 @@ class ColoredLoggingHandler(EventHandler): 'moving_to_fort': 'white', 'moving_to_lured_fort': 'white', 'pokemon_catch_rate': 'white', - 'pokemon_evolve_fail': 'white', 'pokestop_on_cooldown': 'white', 'pokestop_out_of_range': 'white', 'polyline_request': 'white', From 138b66413e45efcef1ae015388b80a2725e7a7d8 Mon Sep 17 00:00:00 2001 From: Oscar Fanelli Date: Thu, 11 Aug 2016 14:38:46 +0200 Subject: [PATCH 108/143] Improved documentation (#3604) - Improved read flow - Added volume sharing of cache folder --- docs/docker.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/docker.md b/docs/docker.md index 735cdd99ec..2b2181bb56 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,25 +1,34 @@ Start by downloading for your platform: [Mac](https://www.docker.com/products/docker#/mac), [Windows](https://www.docker.com/products/docker#/windows), or [Linux](https://www.docker.com/products/docker#/linux). Once you have Docker installed, simply create the various config.json files for your different accounts (e.g. `configs/config-account1.json`) and then create a Docker image for PokemonGo-Bot using the Dockerfile in this [repo](https://hub.docker.com/r/svlentink/pokemongo-bot/). -#Automatic setup +#Setup +##Automatic setup Use this docker hub url: https://hub.docker.com/r/svlentink/pokemongo-bot/ ``` docker pull svlentink/pokemongo-bot ``` -#Manual setup +##Manual setup ``` cd PokemonGo-Bot docker build -t pokemongo-bot . ``` + +#Run + You can verify that the image was created with: ``` docker images ``` +- In case of automatic setup, you'll see an image called: `svlentink/pokemongo-bot` +- In case of manual setup, you'll see an image called: `pokemongo-bot` To run PokemonGo-Bot Docker image you've created, simple run: ``` -docker run --name=pokego-bot1 --rm -it -v $(pwd)/configs/config-account1.json:/usr/src/app/configs/config.json pokemongo-bot +docker run --name=pokego-bot1 --rm -it -v $(pwd)/configs/config-account1.json:/usr/src/app/configs/config.json -v $(pwd)/data:/usr/src/app/data pokemongo-bot ``` + +Replace `pokemongo-bot` with `svlentink/pokemongo-bot` in case you followed automatic setup. + _Check the logs in real-time `docker logs -f pgobot`_ If you want to run multiple accounts with the same Docker image, simply specify different config.json and names in the Docker run command. From 66336308792a6e36bbd297e5e9b42eb98d6ff334 Mon Sep 17 00:00:00 2001 From: devn0ll Date: Thu, 11 Aug 2016 19:34:21 +0200 Subject: [PATCH 109/143] Update installation.md (#3618) * Update installation.md Changed the linux Section and the Ubuntu example, for easyer access (if anyone now how to make a code block collapsable feel free) * Update installation.md removed pastebin link (cause i put it there in the first place) because it's not needed edittet linux section to Installation Linux on the example of Ubuntu (merged it with ubuntu install section) i hope i didn't miss anything will check on mac and windows in the next 24h. I will make a new pr for that and link it here --- docs/installation.md | 75 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 12 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index d469fa85f5..873638ff54 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -49,20 +49,71 @@ To update your project do (in the project folder): `git pull` To update python requirement packages do (in the project folder): `pip install --upgrade -r requirements.txt` -### Installation Linux -(change master to dev for the latest version) - -``` -$ git clone --recursive -b master https://github.com/PokemonGoF/PokemonGo-Bot -$ cd PokemonGo-Bot -$ virtualenv . -$ source bin/activate -$ pip install -r requirements.txt -``` -#### Example Installation for Ubuntu +### Linux Installation +####on the Example of Ubuntu (change dev to master for the lastest master version) -http://pastebin.com/pzPjXT65 +if you are on a different Linux OS you maybe have to adapt things like: + +- package mananger (for example yum instead of apt-get) +- package names + +```bash +##install +#change to root +sudo -i +#go to your home directory with the console +apt-get install build-essential autoconf libtool pkg-config make python-dev python-protobuf python2.7 wget git +#install pip +wget https://bootstrap.pypa.io/get-pip.py +python2.7 get-pip.py +rm -f get-pip.py +#get git repo +git clone --recursive -b dev https://github.com/PokemonGoF/PokemonGo-Bot +cd PokemonGo-Bot +#install and enable virtualenv +#You need to make shure your python version and virtualenv verison work together +#install virtualenv and activate it +pip install virtualenv +virtualenv . +source bin/activate +#then install the requierements +pip install -r requirements.txt + +##get the encryption.so and move to right folder +wget http://pgoapi.com/pgoencrypt.tar.gz +tar -xzvf pgoencrypt.tar.gz +cd pgoencrypt/src/ +make +cd ../../ +#make the encrypt able to load +mv pgoencrypt/src/libencrypt.so encrypt.so + +##edit the configuration file +cp configs/config.json.example configs/config.json +vi configs/config.json +# gedit is possible too with 'gedit configs/config.json' +#edit "google" to "ptc" if you have a pokemon trainer account +#edit all settings + + +##update to newest +#if you need to do more i'll update this file +#make shure virtualenv is enabled and you are in the correct folder +git pull +pip install -r requirements.txt + +##start the bot +./run.sh configs/config.json + +##after reboot or closing the terminal +#at every new start go into the folder of the PokemonGo-Bot by +#going into the folder where you startet installing it an then +cd PokemonGo-Bot +#activate virtualenv and start +source bin/activate +./run.sh configs/config.json +``` ### Installation Mac From f3c0ce3fc85ebd722c393724585191dc54c1f6d6 Mon Sep 17 00:00:00 2001 From: Sander Date: Thu, 11 Aug 2016 19:34:50 +0200 Subject: [PATCH 110/143] moving_to_fort and moving_to_lured_fort now also emit current_position (#3614) arrived_at_fort emits position --- pokemongo_bot/__init__.py | 13 ++++++++++--- pokemongo_bot/cell_workers/move_to_fort.py | 7 ++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 5d10cfba02..f3c9a3bd53 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -183,7 +183,8 @@ def _register_events(self): 'moving_to_fort', parameters=( 'fort_name', - 'distance' + 'distance', + 'current_position' ) ) self.event_manager.register_event( @@ -191,7 +192,8 @@ def _register_events(self): parameters=( 'fort_name', 'distance', - 'lure_distance' + 'lure_distance', + 'current_position' ) ) self.event_manager.register_event( @@ -217,7 +219,12 @@ def _register_events(self): parameters=('status_code',) ) self.event_manager.register_event('pokestop_searching_too_often') - self.event_manager.register_event('arrived_at_fort') + self.event_manager.register_event( + 'arrived_at_fort', + parameters=( + 'current_position' + ) + ) # pokemon stuff self.event_manager.register_event( diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 24ecf5e74a..33dd5cf576 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -61,6 +61,7 @@ def work(self): fort_event_data = { 'fort_name': u"{}".format(fort_name), 'distance': format_dist(dist, unit), + 'current_position': self.bot.position } if self.is_attracted() > 0: @@ -87,9 +88,13 @@ def work(self): if not step_walker.step(): return WorkerResult.RUNNING + arrived_at_fort_data = { + 'current_position': self.bot.position + } self.emit_event( 'arrived_at_fort', - formatted='Arrived at fort.' + formatted='Arrived at fort.', + data=arrived_at_fort_data ) return WorkerResult.SUCCESS From 78649ae81b2201250f0aa48157ae556712f39bde Mon Sep 17 00:00:00 2001 From: joaodragao Date: Thu, 11 Aug 2016 18:40:15 +0100 Subject: [PATCH 111/143] Fix handle soft ban (#3629) * Fix `MoveToFort.config` None to Empty {} dict Whenever the bot ticks, `config.get('enable', True)` is required * Add new contributor --- CONTRIBUTORS.md | 1 + pokemongo_bot/cell_workers/handle_soft_ban.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 7374cfa94d..7606647a3c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -64,3 +64,4 @@ * cmezh * Nivong * kestel + * joaodragao diff --git a/pokemongo_bot/cell_workers/handle_soft_ban.py b/pokemongo_bot/cell_workers/handle_soft_ban.py index 8018b7c33e..cf88eeac83 100644 --- a/pokemongo_bot/cell_workers/handle_soft_ban.py +++ b/pokemongo_bot/cell_workers/handle_soft_ban.py @@ -29,7 +29,7 @@ def work(self): ) if fort_distance > Constants.MAX_DISTANCE_FORT_IS_REACHABLE: - MoveToFort(self.bot, config=None).work() + MoveToFort(self.bot, config={}).work() self.bot.recent_forts = self.bot.recent_forts[0:-1] if forts[0]['id'] in self.bot.fort_timeouts: del self.bot.fort_timeouts[forts[0]['id']] From 1bfabca76050742eefdbca79d9ba1279737c6219 Mon Sep 17 00:00:00 2001 From: mmns Date: Thu, 11 Aug 2016 19:58:46 +0200 Subject: [PATCH 112/143] [config] new tasks in example files (#3457) * add UpdateTitleStats to config examples * add UpdateTitleStats to config examples * add SleepSchedule to config examples all config files are missing SleepSchedule from task see https://github.com/PokemonGoF/PokemonGo-Bot/pokemongo_bot/cell_workers/sleep_schedule.py for more info , { "type": "SleepSchedule", "config": { "time": "22:54", "duration":"7:46", "time_random_offset": "00:24", "duration_random_offset": "00:43" } }, * Forgot one * fix for changes in #3467 * disable new tasks by default, removed duplicate --- configs/config.json.cluster.example | 20 ++++++++++++++++++++ configs/config.json.example | 20 ++++++++++++++++++++ configs/config.json.map.example | 20 ++++++++++++++++++++ configs/config.json.path.example | 20 ++++++++++++++++++++ configs/config.json.pokemon.example | 20 ++++++++++++++++++++ 5 files changed, 100 insertions(+) diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index 73ba037703..bb01bb44f3 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -9,6 +9,16 @@ { "type": "HandleSoftBan" }, + { + "type": "SleepSchedule", + "config": { + "enabled": false, + "time": "22:54", + "duration":"7:46", + "time_random_offset": "00:24", + "duration_random_offset": "00:43" + } + }, { "type": "CollectLevelUpReward" }, @@ -18,6 +28,16 @@ "longer_eggs_first": true } }, + { + "type": "UpdateLiveStats", + "config": { + "enabled": false, + "min_interval": 10, + "stats": ["uptime", "stardust_earned", "xp_earned", "xp_per_hour", "stops_visited"], + "terminal_log": true, + "terminal_title": true + } + }, { "type": "TransferPokemon" }, diff --git a/configs/config.json.example b/configs/config.json.example index 1594a18f5e..d2f28cf18e 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -9,6 +9,16 @@ { "type": "HandleSoftBan" }, + { + "type": "SleepSchedule", + "config": { + "enabled": false, + "time": "22:54", + "duration":"7:46", + "time_random_offset": "00:24", + "duration_random_offset": "00:43" + } + }, { "type": "CollectLevelUpReward" }, @@ -18,6 +28,16 @@ "longer_eggs_first": true } }, + { + "type": "UpdateLiveStats", + "config": { + "enabled": false, + "min_interval": 10, + "stats": ["uptime", "stardust_earned", "xp_earned", "xp_per_hour", "stops_visited"], + "terminal_log": true, + "terminal_title": true + } + }, { "type": "TransferPokemon" }, diff --git a/configs/config.json.map.example b/configs/config.json.map.example index f20fb9f1ac..acb6b2f5ea 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -9,6 +9,16 @@ { "type": "HandleSoftBan" }, + { + "type": "SleepSchedule", + "config": { + "enabled": false, + "time": "22:54", + "duration":"7:46", + "time_random_offset": "00:24", + "duration_random_offset": "00:43" + } + }, { "type": "CollectLevelUpReward" }, @@ -18,6 +28,16 @@ "longer_eggs_first": true } }, + { + "type": "UpdateLiveStats", + "config": { + "enabled": false, + "min_interval": 10, + "stats": ["uptime", "stardust_earned", "xp_earned", "xp_per_hour", "stops_visited"], + "terminal_log": true, + "terminal_title": true + } + }, { "type": "TransferPokemon" }, diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 08867f1967..52286b2809 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -9,6 +9,16 @@ { "type": "HandleSoftBan" }, + { + "type": "SleepSchedule", + "config": { + "enabled": false, + "time": "22:54", + "duration":"7:46", + "time_random_offset": "00:24", + "duration_random_offset": "00:43" + } + }, { "type": "CollectLevelUpReward" }, @@ -18,6 +28,16 @@ "longer_eggs_first": true } }, + { + "type": "UpdateLiveStats", + "config": { + "enabled": false, + "min_interval": 10, + "stats": ["uptime", "stardust_earned", "xp_earned", "xp_per_hour", "stops_visited"], + "terminal_log": true, + "terminal_title": true + } + }, { "type": "TransferPokemon" }, diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 3216ad6681..c271f21fcc 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -9,6 +9,16 @@ { "type": "HandleSoftBan" }, + { + "type": "SleepSchedule", + "config": { + "enabled": false, + "time": "22:54", + "duration":"7:46", + "time_random_offset": "00:24", + "duration_random_offset": "00:43" + } + }, { "type": "CollectLevelUpReward" }, @@ -18,6 +28,16 @@ "longer_eggs_first": true } }, + { + "type": "UpdateLiveStats", + "config": { + "enabled": false, + "min_interval": 10, + "stats": ["uptime", "stardust_earned", "xp_earned", "xp_per_hour", "stops_visited"], + "terminal_log": true, + "terminal_title": true + } + }, { "type": "TransferPokemon" }, From 01ac445d48b5a5adeda84ed58388c525c6314f4a Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Thu, 11 Aug 2016 21:39:34 +0200 Subject: [PATCH 113/143] Revert "moving_to_fort and moving_to_lured_fort now also emit current_position" (#3640) --- pokemongo_bot/__init__.py | 13 +++---------- pokemongo_bot/cell_workers/move_to_fort.py | 7 +------ 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index f3c9a3bd53..5d10cfba02 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -183,8 +183,7 @@ def _register_events(self): 'moving_to_fort', parameters=( 'fort_name', - 'distance', - 'current_position' + 'distance' ) ) self.event_manager.register_event( @@ -192,8 +191,7 @@ def _register_events(self): parameters=( 'fort_name', 'distance', - 'lure_distance', - 'current_position' + 'lure_distance' ) ) self.event_manager.register_event( @@ -219,12 +217,7 @@ def _register_events(self): parameters=('status_code',) ) self.event_manager.register_event('pokestop_searching_too_often') - self.event_manager.register_event( - 'arrived_at_fort', - parameters=( - 'current_position' - ) - ) + self.event_manager.register_event('arrived_at_fort') # pokemon stuff self.event_manager.register_event( diff --git a/pokemongo_bot/cell_workers/move_to_fort.py b/pokemongo_bot/cell_workers/move_to_fort.py index 33dd5cf576..24ecf5e74a 100644 --- a/pokemongo_bot/cell_workers/move_to_fort.py +++ b/pokemongo_bot/cell_workers/move_to_fort.py @@ -61,7 +61,6 @@ def work(self): fort_event_data = { 'fort_name': u"{}".format(fort_name), 'distance': format_dist(dist, unit), - 'current_position': self.bot.position } if self.is_attracted() > 0: @@ -88,13 +87,9 @@ def work(self): if not step_walker.step(): return WorkerResult.RUNNING - arrived_at_fort_data = { - 'current_position': self.bot.position - } self.emit_event( 'arrived_at_fort', - formatted='Arrived at fort.', - data=arrived_at_fort_data + formatted='Arrived at fort.' ) return WorkerResult.SUCCESS From 543226e81b864d0fd714337e7cce1da8ac0a0005 Mon Sep 17 00:00:00 2001 From: Eli White Date: Thu, 11 Aug 2016 13:17:51 -0700 Subject: [PATCH 114/143] Return RUNNING if there are more forts to spin (#3412) --- pokemongo_bot/cell_workers/spin_fort.py | 33 +++++++++++-------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 9422d8ec35..61d3eb02bd 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -27,11 +27,13 @@ def should_run(self): return self.ignore_item_count or self.bot.has_space_for_loot() def work(self): - fort = self.get_fort_in_range() + forts = self.get_forts_in_range() - if not self.should_run() or fort is None: + if not self.should_run() or len(forts) == 0: return WorkerResult.SUCCESS + fort = forts[0] + lat = fort['latitude'] lng = fort['longitude'] @@ -135,11 +137,15 @@ def work(self): ) else: self.bot.fort_timeouts[fort["id"]] = (time.time() + 300) * 1000 # Don't spin for 5m - return 11 + return WorkerResult.ERROR sleep(2) - return 0 - def get_fort_in_range(self): + if len(forts) > 1: + return WorkerResult.RUNNING + + return WorkerResult.SUCCESS + + def get_forts_in_range(self): forts = self.bot.get_forts(order_by_distance=True) for fort in forts: @@ -147,21 +153,12 @@ def get_fort_in_range(self): self.bot.fort_timeouts[fort["id"]] = fort['cooldown_complete_timestamp_ms'] forts.remove(fort) - forts = filter(lambda x: x["id"] not in self.bot.fort_timeouts, forts) - - if len(forts) == 0: - return None - - fort = forts[0] - - distance_to_fort = distance( + forts = filter(lambda fort: fort["id"] not in self.bot.fort_timeouts, forts) + forts = filter(lambda fort: distance( self.bot.position[0], self.bot.position[1], fort['latitude'], fort['longitude'] - ) - - if distance_to_fort <= Constants.MAX_DISTANCE_FORT_IS_REACHABLE: - return fort + ) <= Constants.MAX_DISTANCE_FORT_IS_REACHABLE, forts) - return None + return forts From b033783bc76a849f486ad3e912243f498343bc46 Mon Sep 17 00:00:00 2001 From: extink Date: Thu, 11 Aug 2016 22:44:25 +0200 Subject: [PATCH 115/143] Correct colored logging (#3637) * Rewrite colered logging handler to use python logging * Add all terminal colors and a format reset * Add name to contributors list --- CONTRIBUTORS.md | 1 + .../event_handlers/colored_logging_handler.py | 243 +++++++----------- 2 files changed, 96 insertions(+), 148 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 7606647a3c..725360784b 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -65,3 +65,4 @@ * Nivong * kestel * joaodragao + * extink diff --git a/pokemongo_bot/event_handlers/colored_logging_handler.py b/pokemongo_bot/event_handlers/colored_logging_handler.py index 6c527511ba..cedd0e53dc 100644 --- a/pokemongo_bot/event_handlers/colored_logging_handler.py +++ b/pokemongo_bot/event_handlers/colored_logging_handler.py @@ -1,87 +1,88 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import time -import sys -import struct + +import logging from pokemongo_bot.event_manager import EventHandler + class ColoredLoggingHandler(EventHandler): EVENT_COLOR_MAP = { - 'api_error': 'red', - 'bot_exit': 'red', - 'bot_start': 'green', - 'config_error': 'red', - 'egg_already_incubating': 'yellow', - 'egg_hatched': 'green', - 'future_pokemon_release': 'yellow', - 'incubate': 'green', - 'incubator_already_used': 'yellow', - 'inventory_full': 'yellow', - 'item_discard_fail': 'red', - 'item_discarded': 'green', - 'keep_best_release': 'green', - 'level_up': 'green', - 'level_up_reward': 'green', - 'location_cache_error': 'yellow', - 'location_cache_ignored': 'yellow', - 'login_failed': 'red', - 'login_successful': 'green', - 'lucky_egg_error': 'red', - 'move_to_map_pokemon_encounter': 'green', - 'move_to_map_pokemon_fail': 'red', - 'next_egg_incubates': 'yellow', - 'next_sleep': 'green', - 'no_pokeballs': 'red', - 'pokemon_appeared': 'yellow', - 'pokemon_capture_failed': 'red', - 'pokemon_caught': 'blue', - 'pokemon_evolved': 'green', - 'pokemon_fled': 'red', - 'pokemon_inventory_full': 'red', - 'pokemon_nickname_invalid': 'red', - 'pokemon_not_in_range': 'yellow', - 'pokemon_release': 'green', - 'pokemon_vanished': 'red', - 'pokestop_empty': 'yellow', - 'pokestop_searching_too_often': 'yellow', - 'rename_pokemon': 'green', - 'skip_evolve': 'yellow', - 'softban': 'red', - 'spun_pokestop': 'cyan', - 'threw_berry_failed': 'red', - 'unknown_spin_result': 'red', - 'unset_pokemon_nickname': 'red', - 'vip_pokemon': 'red', + 'api_error': 'red', + 'bot_exit': 'red', + 'bot_start': 'green', + 'config_error': 'red', + 'egg_already_incubating': 'yellow', + 'egg_hatched': 'green', + 'future_pokemon_release': 'yellow', + 'incubate': 'green', + 'incubator_already_used': 'yellow', + 'inventory_full': 'yellow', + 'item_discard_fail': 'red', + 'item_discarded': 'green', + 'keep_best_release': 'green', + 'level_up': 'green', + 'level_up_reward': 'green', + 'location_cache_error': 'yellow', + 'location_cache_ignored': 'yellow', + 'login_failed': 'red', + 'login_successful': 'green', + 'lucky_egg_error': 'red', + 'move_to_map_pokemon_encounter': 'green', + 'move_to_map_pokemon_fail': 'red', + 'next_egg_incubates': 'yellow', + 'next_sleep': 'green', + 'no_pokeballs': 'red', + 'pokemon_appeared': 'yellow', + 'pokemon_capture_failed': 'red', + 'pokemon_caught': 'blue', + 'pokemon_evolved': 'green', + 'pokemon_fled': 'red', + 'pokemon_inventory_full': 'red', + 'pokemon_nickname_invalid': 'red', + 'pokemon_not_in_range': 'yellow', + 'pokemon_release': 'green', + 'pokemon_vanished': 'red', + 'pokestop_empty': 'yellow', + 'pokestop_searching_too_often': 'yellow', + 'rename_pokemon': 'green', + 'skip_evolve': 'yellow', + 'softban': 'red', + 'spun_pokestop': 'cyan', + 'threw_berry_failed': 'red', + 'unknown_spin_result': 'red', + 'unset_pokemon_nickname': 'red', + 'vip_pokemon': 'red', # event names for 'white' still here to remember that these events are already determined its color. - 'arrived_at_cluster': 'white', - 'arrived_at_fort': 'white', - 'bot_sleep': 'white', - 'catchable_pokemon': 'white', - 'found_cluster': 'white', - 'incubate_try': 'white', - 'load_cached_location': 'white', - 'location_found': 'white', - 'login_started': 'white', - 'lured_pokemon_found': 'white', - 'move_to_map_pokemon_move_towards': 'white', - 'move_to_map_pokemon_teleport_back': 'white', - 'move_to_map_pokemon_updated_map': 'white', - 'moving_to_fort': 'white', - 'moving_to_lured_fort': 'white', - 'pokemon_catch_rate': 'white', - 'pokestop_on_cooldown': 'white', - 'pokestop_out_of_range': 'white', - 'polyline_request': 'white', - 'position_update': 'white', - 'set_start_location': 'white', - 'softban_fix': 'white', - 'softban_fix_done': 'white', - 'spun_fort': 'white', - 'threw_berry': 'white', - 'threw_pokeball': 'white', - 'used_lucky_egg': 'white' + 'arrived_at_cluster': 'white', + 'arrived_at_fort': 'white', + 'bot_sleep': 'white', + 'catchable_pokemon': 'white', + 'found_cluster': 'white', + 'incubate_try': 'white', + 'load_cached_location': 'white', + 'location_found': 'white', + 'login_started': 'white', + 'lured_pokemon_found': 'white', + 'move_to_map_pokemon_move_towards': 'white', + 'move_to_map_pokemon_teleport_back': 'white', + 'move_to_map_pokemon_updated_map': 'white', + 'moving_to_fort': 'white', + 'moving_to_lured_fort': 'white', + 'pokemon_catch_rate': 'white', + 'pokemon_evolve_fail': 'white', + 'pokestop_on_cooldown': 'white', + 'pokestop_out_of_range': 'white', + 'polyline_request': 'white', + 'position_update': 'white', + 'set_start_location': 'white', + 'softban_fix': 'white', + 'softban_fix_done': 'white', + 'spun_fort': 'white', + 'threw_berry': 'white', + 'threw_pokeball': 'white', + 'used_lucky_egg': 'white' } CONTINUOUS_EVENT_NAMES = [ 'catchable_pokemon', @@ -89,83 +90,29 @@ class ColoredLoggingHandler(EventHandler): 'spun_fort' ] COLOR_CODE = { - 'red': '91', - 'green': '92', - 'yellow': '93', - 'blue': '94', - 'cyan': '96' + 'gray': '\033[90m', + 'red': '\033[91m', + 'green': '\033[92m', + 'yellow': '\033[93m', + 'blue': '\033[94m', + 'magenta': '\033[95m', + 'cyan': '\033[96m', + 'white': '\033[97m', + 'reset': '\033[0m' } - def __init__(self): - self._last_event = None - try: - # this `try ... except` is for ImportError on Windows - import fcntl - import termios - self._ioctl = fcntl.ioctl - self._TIOCGWINSZ = termios.TIOCGWINSZ - except ImportError: - self._ioctl = None - self._TIOCGWINSZ = None - def handle_event(self, event, sender, level, formatted_msg, data): - # Prepare message string - message = None - if formatted_msg: - try: - message = formatted_msg.decode('utf-8') - except UnicodeEncodeError: - message = formatted_msg - else: - message = '{}'.format(str(data)) - - # Replace message if necessary - if event == 'catchable_pokemon': - message = 'Something rustles nearby!' + logger = logging.getLogger(type(sender).__name__) - # Truncate previous line if same event continues - if event in ColoredLoggingHandler.CONTINUOUS_EVENT_NAMES and self._last_event == event and sys.stdout.isatty(): - # Filling with "' ' * terminal_width" in order to completely clear last line - terminal_width = self._terminal_width() - if terminal_width: - sys.stdout.write('\r{}\r'.format(' ' * terminal_width)) - else: - sys.stdout.write('\r') - else: - sys.stdout.write("\n") - - color_name = None - if event in ColoredLoggingHandler.EVENT_COLOR_MAP: - color_name = ColoredLoggingHandler.EVENT_COLOR_MAP[event] - - # Change color if necessary + color = self.COLOR_CODE['white'] + if event in self.EVENT_COLOR_MAP: + color = self.COLOR_CODE[self.EVENT_COLOR_MAP[event]] if event == 'egg_hatched' and data.get('pokemon', 'error') == 'error': - # `egg_hatched` event will be dispatched in both cases: hatched pokemon info is successfully taken or not. - # change color from 'green' to 'red' in case of error. - color_name = 'red' + color = self.COLOR_CODE['red'] + formatted_msg = '{}{}{}'.format(color, formatted_msg, self.COLOR_CODE['reset']) - if color_name in ColoredLoggingHandler.COLOR_CODE: - sys.stdout.write( - '[{time}] \033[{color}m{message}\033[0m'.format( - time=time.strftime("%H:%M:%S"), - color=ColoredLoggingHandler.COLOR_CODE[color_name], - message=message - ) - ) + if formatted_msg: + message = "[{}] {}".format(event, formatted_msg) else: - sys.stdout.write('[{time}] {message}'.format( - time=time.strftime("%H:%M:%S"), - message=message - )) - - sys.stdout.flush() - self._last_event = event - - def _terminal_width(self): - if self._ioctl is None or self._TIOCGWINSZ is None: - return None - - h, w, hp, wp = struct.unpack(str('HHHH'), - self._ioctl(0, self._TIOCGWINSZ, - struct.pack(str('HHHH'), 0, 0, 0, 0))) - return w + message = '{}: {}'.format(event, str(data)) + getattr(logger, level)(message) From 44d7b39ae3446ddc1af083be6d8dae38d4c68326 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Thu, 11 Aug 2016 22:23:28 +0100 Subject: [PATCH 116/143] Allow to set throw quality and spin odds (#2534) * Allow to set throw quality and spin odds * Throw Parameter : add example * Throw parameter example : update indentation --- configs/config.json.example | 7 ++ pokecli.py | 45 ++++++++++++ .../cell_workers/pokemon_catch_worker.py | 68 +++++++++++++++++-- pokemongo_bot/human_behaviour.py | 19 ------ 4 files changed, 113 insertions(+), 26 deletions(-) diff --git a/configs/config.json.example b/configs/config.json.example index d2f28cf18e..ae1bdb02fe 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -121,6 +121,13 @@ "// Example of always catching Rattata:": {}, "// Rattata": { "always_catch" : true } }, + "catch_throw_parameters": { + "excellent_rate": 0.1, + "great_rate": 0.5, + "nice_rate": 0.3, + "normal_rate": 0.1, + "spin_success_rate" : 0.6, + }, "release": { "any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}, "// Example of always releasing Rattata:": {}, diff --git a/pokecli.py b/pokecli.py index 4544ccf124..2cd5553ba5 100644 --- a/pokecli.py +++ b/pokecli.py @@ -394,6 +394,51 @@ def _json_loader(filename): type=bool, default=True ) + add_config( + parser, + load, + short_flag="-cte", + long_flag="--catch_throw_parameters.excellent_rate", + help="Define the odd of performing an excellent throw", + type=float, + default=1 + ) + add_config( + parser, + load, + short_flag="-ctg", + long_flag="--catch_throw_parameters.great_rate", + help="Define the odd of performing a great throw", + type=float, + default=0 + ) + add_config( + parser, + load, + short_flag="-ctn", + long_flag="--catch_throw_parameters.nice_rate", + help="Define the odd of performing a nice throw", + type=float, + default=0 + ) + add_config( + parser, + load, + short_flag="-ctm", + long_flag="--catch_throw_parameters.normal_rate", + help="Define the odd of performing a normal throw", + type=float, + default=0 + ) + add_config( + parser, + load, + short_flag="-cts", + long_flag="--catch_throw_parameters.spin_success_rate", + help="Define the odds of performing a spin throw (Value between 0 (never) and 1 (always))", + type=float, + default=1 + ) # Start to parse other attrs config = parser.parse_args() diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 77e29ae10b..451ca0e9df 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- import time +from random import random from pokemongo_bot import inventory from pokemongo_bot.base_task import BaseTask -from pokemongo_bot.human_behaviour import normalized_reticle_size, sleep, spin_modifier +from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.worker_result import WorkerResult CATCH_STATUS_SUCCESS = 1 @@ -69,6 +70,7 @@ def work(self, response_dict=None): # validate response if not response_dict: return WorkerResult.ERROR + try: responses = response_dict['responses'] response = responses[self.response_key] @@ -301,8 +303,18 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and berry_count > 0 and not used_berry: catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) berry_count -= 1 + + # Randomize the quality of the throw + # Default structure + throw_parameters = {'normalized_reticle_size': 1.950, + 'spin_modifier': 1.0, + 'normalized_hit_position': 1.0, + 'throw_type_label': 'Excellent'} + self.generate_spin_parameter(throw_parameters) + self.generate_throw_quality_parameters(throw_parameters) # try to catch pokemon! + # TODO : Log which type of throw we selected items_stock[current_ball] -= 1 self.emit_event( 'threw_pokeball', @@ -314,17 +326,14 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): } ) - reticle_size_parameter = normalized_reticle_size(self.config.catch_randomize_reticle_factor) - spin_modifier_parameter = spin_modifier(self.config.catch_randomize_spin_factor) - response_dict = self.api.catch_pokemon( encounter_id=encounter_id, pokeball=current_ball, - normalized_reticle_size=reticle_size_parameter, + normalized_reticle_size=throw_parameters['normalized_reticle_size'], spawn_point_id=self.spawn_point_guid, hit_pokemon=1, - spin_modifier=spin_modifier_parameter, - normalized_hit_position=1 + spin_modifier=throw_parameters['spin_modifier'], + normalized_hit_position=throw_parameters['normalized_hit_position'] ) try: @@ -392,3 +401,48 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): self.bot.softban = False break + + def generate_spin_parameter(self, throw_parameters): + spin_success_rate = self.config.catch_throw_parameters_spin_success_rate + if random() <= spin_success_rate: + throw_parameters['spin_modifier'] = 0.5 + 0.5 * random() + else: + throw_parameters['spin_modifier'] = 0.499 * random() + + def generate_throw_quality_parameters(self, throw_parameters): + throw_excellent_chance = self.config.catch_throw_parameters_excellent_rate + throw_great_chance = self.config.catch_throw_parameters_great_rate + throw_nice_chance = self.config.catch_throw_parameters_nice_rate + throw_normal_throw_chance = self.config.catch_throw_parameters_normal_rate + + # Total every chance types, pick a random number in the range and check what type of throw we got + total_chances = throw_excellent_chance + throw_great_chance \ + + throw_nice_chance + throw_normal_throw_chance + + random_throw = random() * total_chances + + if random_throw <= throw_excellent_chance: + throw_parameters['normalized_reticle_size'] = 1.70 + 0.25 * random() + throw_parameters['normalized_hit_position'] = 1.0 + throw_parameters['throw_type_label'] = 'Excellent' + return + + random_throw -= throw_excellent_chance + if random_throw <= throw_great_chance: + throw_parameters['normalized_reticle_size'] = 1.30 + 0.399 * random() + throw_parameters['normalized_hit_position'] = 1.0 + throw_parameters['throw_type_label'] = 'Great' + return + + random_throw -= throw_great_chance + if random_throw <= throw_nice_chance: + throw_parameters['normalized_reticle_size'] = 1.00 + 0.299 * random() + throw_parameters['normalized_hit_position'] = 1.0 + throw_parameters['throw_type_label'] = 'Nice' + return + + # Not a any kind of special throw, let's throw a normal one + # Here the reticle size doesn't matter, we scored out of it + throw_parameters['normalized_reticle_size'] = 1.25 + 0.70 * random() + throw_parameters['normalized_hit_position'] = 0.0 + throw_parameters['throw_type_label'] = 'Normal' diff --git a/pokemongo_bot/human_behaviour.py b/pokemongo_bot/human_behaviour.py index 2a8d2d5e9f..37a95081ca 100644 --- a/pokemongo_bot/human_behaviour.py +++ b/pokemongo_bot/human_behaviour.py @@ -25,22 +25,3 @@ def random_lat_long_delta(): # should be 364,000 * .000025 = 9.1. So it returns between [-9.1, 9.1] return ((random() * 0.00001) - 0.000005) * 5 - -# Humanized `normalized_reticle_size` parameter for `catch_pokemon` API. -# 1.0 => normal, 1.950 => excellent -def normalized_reticle_size(factor): - minimum = 1.0 - maximum = 1.950 - return uniform( - minimum + (maximum - minimum) * factor, - maximum) - - -# Humanized `spin_modifier` parameter for `catch_pokemon` API. -# 0.0 => normal ball, 1.0 => super spin curve ball -def spin_modifier(factor): - minimum = 0.0 - maximum = 1.0 - return uniform( - minimum + (maximum - minimum) * factor, - maximum) From c344fe294e6c4baa08403d961f0f3fb5b4f4749b Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Thu, 11 Aug 2016 23:40:01 +0200 Subject: [PATCH 117/143] sending location update if distance to move is smaller than step size --- pokemongo_bot/step_walker.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pokemongo_bot/step_walker.py b/pokemongo_bot/step_walker.py index 7727dc6a0c..97a4f14b47 100644 --- a/pokemongo_bot/step_walker.py +++ b/pokemongo_bot/step_walker.py @@ -39,6 +39,17 @@ def __init__(self, bot, speed, dest_lat, dest_lng): def step(self): if (self.dLat == 0 and self.dLng == 0) or self.dist < self.speed: self.api.set_position(self.destLat, self.destLng, 0) + self.bot.event_manager.emit( + 'position_update', + sender=self, + level='debug', + data={ + 'current_position': (self.destLat, self.destLng), + 'last_position': (self.initLat, self.initLng), + 'distance': '', + 'distance_unit': '' + } + ) self.bot.heartbeat() return True From c1517fdd36c46f358ae90277e16c527d0d42df71 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Fri, 12 Aug 2016 00:03:27 +0200 Subject: [PATCH 118/143] removing useless comma --- configs/config.json.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configs/config.json.example b/configs/config.json.example index ae1bdb02fe..de2436807a 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -126,7 +126,7 @@ "great_rate": 0.5, "nice_rate": 0.3, "normal_rate": 0.1, - "spin_success_rate" : 0.6, + "spin_success_rate" : 0.6 }, "release": { "any": {"release_below_cp": 0, "release_below_iv": 0, "logic": "or"}, From ca4a1977a80d544fa3afd331d32fd60399f1f621 Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Fri, 12 Aug 2016 00:04:02 +0200 Subject: [PATCH 119/143] removing old catch randomizer stuff --- configs/config.json.example | 2 -- 1 file changed, 2 deletions(-) diff --git a/configs/config.json.example b/configs/config.json.example index de2436807a..502c18c78e 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -113,8 +113,6 @@ "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, - "catch_randomize_reticle_factor": 1.0, - "catch_randomize_spin_factor": 1.0, "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, From b8af467f8e694adf3e2353876e27159d4f16daee Mon Sep 17 00:00:00 2001 From: Brice Date: Fri, 12 Aug 2016 00:08:15 +0200 Subject: [PATCH 120/143] Improved item recycling (#2482) * Now recycling only if less than 5 space left in inventory Now trying to recycle before moving to/spinning fort if bags are almost full Refactored recycle_items * Removed recycling before moving to/spinning fort if bags are almost full * Removed recycling before moving to/spinning fort if bags are almost full * Removed unused import * Now recycling only if less than 5 space left in inventory Now trying to recycle before moving to/spinning fort if bags are almost full Refactored recycle_items * Removed recycling before moving to/spinning fort if bags are almost full * Added documentation Replace all `logger.log` calls with events! (#2173) * Deleted change on files not concerned * Deleted change on files not concerned * The inner class is now "private" * new class to centralize inventory management * use new inventory class in evolve_pokemon * use new inventory to display # candy after catch * Now using the new inventory (#2528) * Fixed #3256 * Merge branch 'dev' of https://github.com/PokemonGoF/PokemonGo-Bot into PokemonGoF-dev # Conflicts: # pokemongo_bot/cell_workers/recycle_items.py Added methods in the inventory manager needed for the recycle_items task * Fixed error if item_count result is false * Now keeps track of item inventory * Moved inventory update in request_recycle method * Minor comment change * Fixed not running if had more item than inventory size (#3531) * Added to the inventory class the necessary to keep trace of items * Now using the new inventory class properly * Decoupled when to recycle an item from how to do it. * Moved the recycler in the services folder --- pokemongo_bot/cell_workers/recycle_items.py | 168 +++++++++++------- pokemongo_bot/inventory.py | 65 ++++++- pokemongo_bot/services/__init__.py | 0 pokemongo_bot/services/item_recycle_worker.py | 109 ++++++++++++ 4 files changed, 277 insertions(+), 65 deletions(-) create mode 100644 pokemongo_bot/services/__init__.py create mode 100644 pokemongo_bot/services/item_recycle_worker.py diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 3232870d03..4b6ea8c3ca 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -1,85 +1,129 @@ import json import os + +from pokemongo_bot import inventory from pokemongo_bot.base_dir import _base_dir from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.services.item_recycle_worker import ItemRecycler from pokemongo_bot.tree_config_builder import ConfigException +from pokemongo_bot.worker_result import WorkerResult +DEFAULT_MIN_EMPTY_SPACE = 6 class RecycleItems(BaseTask): SUPPORTED_TASK_API_VERSION = 1 + """ + Recycle undesired items if there is less than five space in inventory. + You can use either item's name or id. For the full list of items see ../../data/items.json + + It's highly recommended to put this task before move_to_fort and spin_fort task in the config file so you'll most likely be able to loot. + + Example config : + { + "type": "RecycleItems", + "config": { + "min_empty_space": 6, # 6 by default + "item_filter": { + "Pokeball": {"keep": 20}, + "Greatball": {"keep": 50}, + "Ultraball": {"keep": 100}, + "Potion": {"keep": 0}, + "Super Potion": {"keep": 0}, + "Hyper Potion": {"keep": 20}, + "Max Potion": {"keep": 50}, + "Revive": {"keep": 0}, + "Max Revive": {"keep": 20}, + "Razz Berry": {"keep": 20} + } + } + } + """ + def initialize(self): + self.items_filter = self.config.get('item_filter', {}) self.min_empty_space = self.config.get('min_empty_space', None) - self.item_filter = self.config.get('item_filter', {}) self._validate_item_filter() def _validate_item_filter(self): + """ + Validate user's item filter config + :return: Nothing. + :rtype: None + :raise: ConfigException: When an item doesn't exist in ../../data/items.json + """ item_list = json.load(open(os.path.join(_base_dir, 'data', 'items.json'))) - for config_item_name, bag_count in self.item_filter.iteritems(): + for config_item_name, bag_count in self.items_filter.iteritems(): if config_item_name not in item_list.viewvalues(): if config_item_name not in item_list: raise ConfigException( "item {} does not exist, spelling mistake? (check for valid item names in data/items.json)".format( config_item_name)) + def should_run(self): + """ + Returns a value indicating whether the recycling process should be run. + :return: True if the recycling process should be run; otherwise, False. + :rtype: bool + """ + if inventory.items().get_space_left() < (DEFAULT_MIN_EMPTY_SPACE if self.min_empty_space is None else self.min_empty_space): + return True + return False + def work(self): - items_in_bag = self.bot.get_inventory_count('item') - total_bag_space = self.bot.player_data['max_item_storage'] - free_bag_space = total_bag_space - items_in_bag - - if self.min_empty_space is not None: - if free_bag_space >= self.min_empty_space and items_in_bag < total_bag_space: - self.emit_event( - 'item_discard_skipped', - formatted="Skipping Recycling of Items. {space} space left in bag.", - data={ - 'space': free_bag_space - } - ) - return - - self.bot.latest_inventory = None - item_count_dict = self.bot.item_inventory_count('all') - - for item_id, bag_count in item_count_dict.iteritems(): - item_name = self.bot.item_list[str(item_id)] - id_filter = self.item_filter.get(item_name, 0) - if id_filter is not 0: - id_filter_keep = id_filter.get('keep', 20) - else: - id_filter = self.item_filter.get(str(item_id), 0) - if id_filter is not 0: - id_filter_keep = id_filter.get('keep', 20) - - bag_count = self.bot.item_inventory_count(item_id) - if (item_name in self.item_filter or str(item_id) in self.item_filter) and bag_count > id_filter_keep: - items_recycle_count = bag_count - id_filter_keep - response_dict_recycle = self.send_recycle_item_request(item_id=item_id, count=items_recycle_count) - result = response_dict_recycle.get('responses', {}).get('RECYCLE_INVENTORY_ITEM', {}).get('result', 0) - - if result == 1: # Request success - self.emit_event( - 'item_discarded', - formatted='Discarded {amount}x {item} (maximum {maximum}).', - data={ - 'amount': str(items_recycle_count), - 'item': item_name, - 'maximum': str(id_filter_keep) - } - ) - else: - self.emit_event( - 'item_discard_fail', - formatted="Failed to discard {item}", - data={ - 'item': item_name - } - ) - - def send_recycle_item_request(self, item_id, count): - # Example of good request response - # {'responses': {'RECYCLE_INVENTORY_ITEM': {'result': 1, 'new_count': 46}}, 'status_code': 1, 'auth_ticket': {'expire_timestamp_ms': 1469306228058L, 'start': '/HycFyfrT4t2yB2Ij+yoi+on778aymMgxY6RQgvrGAfQlNzRuIjpcnDd5dAxmfoTqDQrbz1m2dGqAIhJ+eFapg==', 'end': 'f5NOZ95a843tgzprJo4W7Q=='}, 'request_id': 8145806132888207460L} - return self.bot.api.recycle_inventory_item( - item_id=item_id, - count=count - ) + """ + Discard items if necessary. + :return: Returns wether or not the task went well + :rtype: WorkerResult + """ + # TODO: Use new inventory everywhere and then remove the inventory update + # Updating inventory + inventory.refresh_inventory() + worker_result = WorkerResult.SUCCESS + if self.should_run(): + + # For each user's item in inventory recycle it if needed + for item_in_inventory in inventory.items().all(): + amount_to_recycle = self.get_amount_to_recycle(item_in_inventory) + + if self.item_should_be_recycled(item_in_inventory, amount_to_recycle): + if ItemRecycler(self.bot, item_in_inventory, amount_to_recycle).work() == WorkerResult.ERROR: + worker_result = WorkerResult.ERROR + + return worker_result + + def item_should_be_recycled(self, item, amount_to_recycle): + """ + Returns a value indicating whether the item should be recycled. + :param amount_to_recycle: + :param item: + :return: True if the title should be recycled; otherwise, False. + :rtype: bool + """ + return (item.name in self.items_filter or str( + item.id) in self.items_filter) and amount_to_recycle > 0 + + def get_amount_to_recycle(self, item): + """ + Determine the amount to recycle accordingly to user config + :param item: Item to determine the amount to recycle + :return: The amount to recycle + :rtype: int + """ + amount_to_keep = self.get_amount_to_keep(item) + return 0 if amount_to_keep is None else item.count - amount_to_keep + + def get_amount_to_keep(self, item): + """ + Determine item's amount to keep in inventory. + :param item: + :return: Item's amount to keep in inventory. + :rtype: int + """ + item_filter_config = self.items_filter.get(item.name, 0) + if item_filter_config is not 0: + return item_filter_config.get('keep', 20) + else: + item_filter_config = self.items_filter.get(str(item.id), 0) + if item_filter_config is not 0: + return item_filter_config.get('keep', 20) diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index d7f890933f..3ade99c0af 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1,7 +1,6 @@ import json import logging import os - from pokemongo_bot.base_dir import _base_dir ''' @@ -101,14 +100,59 @@ def captured(self, pokemon_id): return False return self._data[pokemon_id]['times_captured'] > 0 +class Item(object): + def __init__(self, item_id, item_count): + self.id = item_id + self.name = Items.name_for(self.id) + self.count = item_count + + def remove(self, amount): + if self.count < amount: + raise Exception('Tried to remove more {} than you have'.format(self.name)) + self.count -= amount + + def add(self, amount): + if amount < 0: + raise Exception('Must add positive amount of {}'.format(self.name)) + self.count += amount + class Items(_BaseInventoryComponent): TYPE = 'item' ID_FIELD = 'item_id' STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'items.json') - def count_for(self, item_id): - return self._data[item_id]['count'] + def parse(self, item_data): + item_id = item_data.get(Items.ID_FIELD, None) + item_count = item_data['count'] if 'count' in item_data else 0 + return Item(item_id, item_count) + + def get(self, item_id): + return self._data.setdefault(item_id, Item(item_id, 0)) + + @classmethod + def name_for(cls, item_id): + return cls.STATIC_DATA[str(item_id)] + + def get_space_used(self): + """ + Counts the space used in item inventory. + :return: The space used in item inventory. + :rtype: int + """ + space_used = 1 + for item_in_inventory in _inventory.items.all(): + space_used += item_in_inventory.count + return space_used + + def get_space_left(self): + """ + Compute the space left in item inventory. + :return: The space left in item inventory. + :rtype: int + """ + _inventory.retrieve_item_inventory_size() + return _inventory.item_inventory_size - self.get_space_used() class Pokemons(_BaseInventoryComponent): @@ -749,6 +793,7 @@ def __init__(self, bot): self.items = Items() self.pokemons = Pokemons() self.refresh() + self.item_inventory_size = None def refresh(self): # TODO: it would be better if this class was used for all @@ -761,6 +806,16 @@ def refresh(self): user_web_inventory = os.path.join(_base_dir, 'web', 'inventory-%s.json' % (self.bot.config.username)) with open(user_web_inventory, 'w') as outfile: json.dump(inventory, outfile) + def retrieve_item_inventory_size(self): + """ + Retrieves the item inventory size + :return: Nothing. + :rtype: None + """ + # TODO: Force update of _item_inventory_size if the player upgrades its size + if self.item_inventory_size is None: + self.item_inventory_size = self.bot.api.get_player()['responses']['GET_PLAYER']['player_data']['max_item_storage'] + # # Usage helpers @@ -768,6 +823,7 @@ def refresh(self): # STAB (Same-type attack bonus) STAB_FACTOR = 1.25 + _inventory = None LevelToCPm() # init LevelToCPm FastAttacks() # init FastAttacks @@ -812,6 +868,9 @@ def init_inventory(bot): def refresh_inventory(): _inventory.refresh() +def get_item_inventory_size(): + _inventory.retrieve_item_inventory_size() + return _inventory.item_inventory_size def pokedex(): return _inventory.pokedex diff --git a/pokemongo_bot/services/__init__.py b/pokemongo_bot/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pokemongo_bot/services/item_recycle_worker.py b/pokemongo_bot/services/item_recycle_worker.py new file mode 100644 index 0000000000..e59270d4ea --- /dev/null +++ b/pokemongo_bot/services/item_recycle_worker.py @@ -0,0 +1,109 @@ +from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.base_task import BaseTask +from pokemongo_bot import inventory +from pokemongo_bot.tree_config_builder import ConfigException + +RECYCLE_REQUEST_RESPONSE_SUCCESS = 1 +class ItemRecycler(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + """ + This class contains details of recycling process. + """ + def __init__(self, bot, item_to_recycle, amount_to_recycle): + """ + Initialise an instance of ItemRecycler + :param bot: The instance of the Bot + :param item_to_recycle: The item to recycle + :type item_to_recycle: Item + :param amount_to_recycle: The amount to recycle + :type amount_to_recycle: int + :return: Nothing. + :rtype: None + """ + self.bot = bot + self.item_to_recycle = item_to_recycle + self.amount_to_recycle = amount_to_recycle + self.recycle_item_request_result = None + + def work(self): + """ + Recycle an item + :return: Returns wether or not the task went well + :rtype: WorkerResult + """ + if self.should_run(): + self.request_recycle() + if self.is_recycling_success(): + self._update_inventory() + self._emit_recycle_succeed() + return WorkerResult.SUCCESS + else: + self._emit_recycle_failed() + return WorkerResult.ERROR + + def should_run(self): + """ + Returns a value indicating whether or mot the recycler should be run. + :return: True if the recycler should be run; otherwise, False. + :rtype: bool + """ + if self.amount_to_recycle > 0 and self.item_to_recycle is not None: + return True + return False + + def request_recycle(self): + """ + Request recycling of the item and store api call response's result. + :return: Nothing. + :rtype: None + """ + response = self.bot.api.recycle_inventory_item(item_id=self.item_to_recycle.id, + count=self.amount_to_recycle) + # Example of good request response + # {'responses': {'RECYCLE_INVENTORY_ITEM': {'result': 1, 'new_count': 46}}, 'status_code': 1, 'auth_ticket': {'expire_timestamp_ms': 1469306228058L, 'start': '/HycFyfrT4t2yB2Ij+yoi+on778aymMgxY6RQgvrGAfQlNzRuIjpcnDd5dAxmfoTqDQrbz1m2dGqAIhJ+eFapg==', 'end': 'f5NOZ95a843tgzprJo4W7Q=='}, 'request_id': 8145806132888207460L} + self.recycle_item_request_result = response.get('responses', {}).get('RECYCLE_INVENTORY_ITEM', {}).get('result', 0) + + def _update_inventory(self): + """ + Updates the inventory. Prevent an unnecessary call to the api + :return: Nothing. + :rtype: None + """ + inventory.items().get(self.item_to_recycle.id).remove(self.amount_to_recycle) + + def is_recycling_success(self): + """ + Returns a value indicating whether or not the item has been successfully recycled. + :return: True if the item has been successfully recycled; otherwise, False. + :rtype: bool + """ + return self.recycle_item_request_result == RECYCLE_REQUEST_RESPONSE_SUCCESS + + def _emit_recycle_succeed(self): + """ + Emits recycle succeed event in logs + :return: Nothing. + :rtype: None + """ + self.emit_event( + 'item_discarded', + formatted='Discarded {amount}x {item}).', + data={ + 'amount': str(self.amount_to_recycle), + 'item': self.item_to_recycle.name, + } + ) + + def _emit_recycle_failed(self): + """ + Emits recycle failed event in logs + :return: Nothing. + :rtype: None + """ + self.emit_event( + 'item_discard_fail', + formatted="Failed to discard {item}", + data={ + 'item': self.item_to_recycle.name + } + ) From f83e76777ae13bde1e4ee2470c6c1b3f31ebb7ac Mon Sep 17 00:00:00 2001 From: hongxu Date: Fri, 12 Aug 2016 10:34:47 +0800 Subject: [PATCH 121/143] remove Debian python-protobuf dependency (#3670) - during installation of pgoapi, protobuf will automatically be a dependency (confirmed with `pip show pgoapi`) - Even for Ubuntu 16.04 LTS, python-protobuf version is 2.6.1-1.3, which is too old + confirmed with `apt-cache show python-protobuf` + see issue #1815 (https://github.com/PokemonGoF/PokemonGo-Bot/issues/1815) --- Dockerfile | 2 -- install.sh | 2 +- setup.sh | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2520813273..141325d60e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,6 @@ RUN echo $timezone > /etc/timezone \ && ln -sfn /usr/share/zoneinfo/$timezone /etc/localtime \ && dpkg-reconfigure -f noninteractive tzdata -RUN apt-get update \ - && apt-get install -y python-protobuf RUN cd /tmp && wget "http://pgoapi.com/pgoencrypt.tar.gz" \ && tar zxvf pgoencrypt.tar.gz \ && cd pgoencrypt/src \ diff --git a/install.sh b/install.sh index 1e7415ed30..f11e58e136 100755 --- a/install.sh +++ b/install.sh @@ -5,7 +5,7 @@ if [ -f /etc/debian_version ] then echo "You are on Debian/Ubuntu" sudo apt-get update -sudo apt-get -y install python python-pip python-dev build-essential git python-protobuf virtualenv +sudo apt-get -y install python python-pip python-dev build-essential git virtualenv elif [ -f /etc/redhat-release ] then echo "You are on CentOS/RedHat" diff --git a/setup.sh b/setup.sh index c5acdf9c8b..c9ec2a1c5e 100755 --- a/setup.sh +++ b/setup.sh @@ -55,7 +55,7 @@ if [ -f /etc/debian_version ] then echo "You are on Debian/Ubuntu" sudo apt-get update -sudo apt-get -y install python python-pip python-dev build-essential git python-protobuf virtualenv +sudo apt-get -y install python python-pip python-dev build-essential git virtualenv elif [ -f /etc/redhat-release ] then echo "You are on CentOS/RedHat" From b8ea3681013b152f472fcec89b9d83436e33cff8 Mon Sep 17 00:00:00 2001 From: Anakin5 Date: Fri, 12 Aug 2016 15:19:04 +0800 Subject: [PATCH 122/143] First basic features of the pokemon optimizer (#2956) * catching every single pokemon nearby * catch lured pokemon in all forts nearby * adding run_interval to some tasks to avoid running all the time and minimum tick time of 5 seconds Tasks inheriting from BaseTask should use `self._update_last_ran` and `_time_to_run` if they want to implement the time based running. The config to set a custom timer is named `run_interval`. * added config to ignore item count for Spin and MoveToFort this works good with the `run_interval` configuration added to TransferPokemon and RecycleItem * spinning all pokestops in range * fixing loop in spin fort task * First basic features of the pokemon optimizer * For now, dry run only * Add cygwin to supported platform and improved log readability (#2948) * Add cygwin to supported platform and improved log readability * fixed formatting * - Add dry_run and use_lucky_egg in config - Evolve all pokemons together and only if enough for a full lucky egg (90). - Keep enough candies for consecutive evolutions of best pokemons - Only evolve the lowest rank of a family * Add lucky egg support when enough pokemon to evolve * fixing returns * - Support Eevee evolution scheme - Rename "use_lucky_egg" parameter in the more accurate "evolve_only_with_lucky_egg" * Revert "Merge remote-tracking branch 'origin/faeture/xp-improvements' into pokemon_optimizer" This reverts commit ff1f5e4bd3ec66b904625ec26b969f57ae6aaeb8, reversing changes made to e8fd90137e53409e87f8fdcf341916cf6d551481. * - Fix an issue in evolve_pokemon task - Use common inventory - Add configuration example * Add missing inventory refresh at the end of the process * Add missing inventory refresh after catching a pokemon * Add parameters "transfer" and "evolve" to activate/deactivate corresponding action. If both false, this is equivalent to a dry_run. Add parameters "use_lucky_egg" to allow task to use a lucky egg before evolve. Add parameter "minimum_evolve_for_lucky_egg" to add a requirement on the number of evolution before using a lucky egg. * Move some functions around * Default lucky egg to false + had again parameter "evolve_only_with_lucky_egg" * Fix qn issue with egg counting Add configuration parameter to allow customization of how pokemons are ranked in a family * Update configuration example * Upgrade to latest inventory * Fix bug --- .gitignore | 1 + configs/config.json.optimizer.example | 105 +++++++ pokemongo_bot/__init__.py | 2 +- pokemongo_bot/cell_workers/__init__.py | 1 + pokemongo_bot/cell_workers/evolve_pokemon.py | 3 +- .../cell_workers/pokemon_catch_worker.py | 3 +- .../cell_workers/pokemon_optimizer.py | 285 ++++++++++++++++++ 7 files changed, 396 insertions(+), 4 deletions(-) create mode 100644 configs/config.json.optimizer.example create mode 100644 pokemongo_bot/cell_workers/pokemon_optimizer.py diff --git a/.gitignore b/.gitignore index 06973c1249..cfba4942e0 100644 --- a/.gitignore +++ b/.gitignore @@ -119,6 +119,7 @@ configs/* !configs/config.json.map.example !configs/path.example.json !config.json.cluster.example +!config.json.optimizer.example # Virtualenv folders bin/ diff --git a/configs/config.json.optimizer.example b/configs/config.json.optimizer.example new file mode 100644 index 0000000000..f63a2b7ae1 --- /dev/null +++ b/configs/config.json.optimizer.example @@ -0,0 +1,105 @@ +{ + "auth_service": "google", + "username": "YOUR_USERNAME", + "password": "YOUR_PASSWORD", + "location": "SOME_LOCATION", + "gmapkey": "GOOGLE_MAPS_API_KEY", + "tasks": [ + { + "type": "HandleSoftBan" + }, + { + "type": "CollectLevelUpReward" + }, + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + }, + { + "type": "PokemonOptimizer", + "config": { + "transfer": true, + "evolve": true, + "use_lucky_egg": true, + "evolve_only_with_lucky_egg": true, + "minimum_evolve_for_lucky_egg": 90, + "keep": [ + { + "top": 1, + "evolve": true, + "// Available sorting keys are:": true, + "// iv, cp, ncp, ivcp, max_cp, iv_attack, iv_defense, iv_stamina, hp_max, level": true, + "sort": ["iv"] + }, + { + "top": 1, + "evolve": true, + "sort": ["ncp"] + }, + { + "top": 1, + "evolve": false, + "sort": ["cp"] + } + ] + } + }, + { + "type": "RecycleItems", + "config": { + "min_empty_space": 15, + "item_filter": { + "Pokeball": { "keep": 100 }, + "Potion": { "keep": 10 }, + "Super Potion": { "keep": 20 }, + "Hyper Potion": { "keep": 30 }, + "Revive": { "keep": 30 }, + "Razz Berry": { "keep": 100 } + } + } + }, + { + "type": "CatchVisiblePokemon" + }, + { + "type": "CatchLuredPokemon" + }, + { + "type": "SpinFort", + "config": { + "ignore_item_count": true + } + }, + { + "type": "MoveToFort", + "config": { + "lure_attraction": false, + "lure_max_distance": 2000, + "ignore_item_count": true + } + } + ], + "map_object_cache_time": 5, + "forts": { + "avoid_circles": true, + "max_circle_size": 50 + }, + "websocket_server": true, + "walk": 4.16, + "action_wait_min": 1, + "action_wait_max": 4, + "debug": false, + "test": false, + "health_record": false, + "location_cache": true, + "distance_unit": "km", + "reconnecting_timeout": 15, + "logging_color": true, + "catch": { + "any": { + "always_catch": true + } + } +} diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 5d10cfba02..f49d698464 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -299,7 +299,7 @@ def _register_events(self): ) self.event_manager.register_event( 'pokemon_evolved', - parameters=('pokemon', 'iv', 'cp') + parameters=('pokemon', 'iv', 'cp', 'xp') ) self.event_manager.register_event('skip_evolve') self.event_manager.register_event('threw_berry_failed', parameters=('status_code',)) diff --git a/pokemongo_bot/cell_workers/__init__.py b/pokemongo_bot/cell_workers/__init__.py index 7933425b63..538673cfe7 100644 --- a/pokemongo_bot/cell_workers/__init__.py +++ b/pokemongo_bot/cell_workers/__init__.py @@ -8,6 +8,7 @@ from move_to_map_pokemon import MoveToMapPokemon from nickname_pokemon import NicknamePokemon from pokemon_catch_worker import PokemonCatchWorker +from pokemon_optimizer import PokemonOptimizer from transfer_pokemon import TransferPokemon from recycle_items import RecycleItems from spin_fort import SpinFort diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 7380f1c5db..3f764f9d58 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -106,7 +106,8 @@ def _execute_pokemon_evolve(self, pokemon, cache): data={ 'pokemon': pokemon.name, 'iv': pokemon.iv, - 'cp': pokemon.cp + 'cp': pokemon.cp, + 'xp': 0 } ) inventory.candies().get(pokemon.pokemon_id).consume(pokemon.evolution_cost) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 451ca0e9df..d28f3f58fd 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -387,8 +387,7 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): ) # We could refresh here too, but adding 3 saves a inventory request - candy = inventory.candies().get(pokemon.num) - candy.add(3) + candy = inventory.candies(True).get(pokemon.num) self.emit_event( 'gained_candy', formatted='You now have {quantity} {type} candy!', diff --git a/pokemongo_bot/cell_workers/pokemon_optimizer.py b/pokemongo_bot/cell_workers/pokemon_optimizer.py new file mode 100644 index 0000000000..ba7072eb2d --- /dev/null +++ b/pokemongo_bot/cell_workers/pokemon_optimizer.py @@ -0,0 +1,285 @@ +import copy +import logging + +from pokemongo_bot import inventory +from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.human_behaviour import sleep, action_delay +from pokemongo_bot.item_list import Item +from pokemongo_bot.worker_result import WorkerResult + + +class PokemonOptimizer(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + + def initialize(self): + self.family_by_family_id = {} + self.logger = logging.getLogger(self.__class__.__name__) + + self.config_transfer = self.config.get("transfer", False) + self.config_evolve = self.config.get("evolve", False) + self.config_use_lucky_egg = self.config.get("use_lucky_egg", False) + self.config_evolve_only_with_lucky_egg = self.config.get("evolve_only_with_lucky_egg", True) + self.config_minimum_evolve_for_lucky_egg = self.config.get("minimum_evolve_for_lucky_egg", 90) + self.config_keep = self.config.get("keep", [{"top": 1, "evolve": True, "sort": ["iv"]}, + {"top": 1, "evolve": True, "sort": ["ncp"]}, + {"top": 1, "evolve": False, "sort": ["cp"]}]) + + def get_pokemon_slot_left(self): + return self.bot._player["max_pokemon_storage"] - len(inventory.pokemons()._data) + + def work(self): + if self.get_pokemon_slot_left() > 5: + return WorkerResult.SUCCESS + + self.parse_inventory() + + transfer_all = [] + evo_all_best = [] + evo_all_crap = [] + + for family_id, family in self.family_by_family_id.items(): + transfer, evo_best, evo_crap = self.get_family_optimized(family_id, family) + transfer_all += transfer + evo_all_best += evo_best + evo_all_crap += evo_crap + + evo_all = evo_all_best + evo_all_crap + + self.apply_optimization(transfer_all, evo_all) + inventory.refresh_inventory() + + return WorkerResult.SUCCESS + + def parse_inventory(self): + self.family_by_family_id.clear() + + for pokemon in inventory.pokemons().all(): + family_id = pokemon.first_evolution_id + setattr(pokemon, "ncp", pokemon.cp_percent) + + self.family_by_family_id.setdefault(family_id, []).append(pokemon) + + def get_family_optimized(self, family_id, family): + if family_id == 133: # "Eevee" + return self.get_multi_family_optimized(family_id, family, 3) + + evolve_best = [] + keep_best = [] + + for criteria in self.config_keep: + if criteria.get("evolve", True): + evolve_best += self.get_top_rank(family, criteria) + else: + keep_best += self.get_top_rank(family, criteria) + + evolve_best = self.unique_pokemons(evolve_best) + keep_best = self.unique_pokemons(keep_best) + + return self.get_evolution_plan(family_id, family, evolve_best, keep_best) + + def get_multi_family_optimized(self, family_id, family, nb_branch): + # Transfer each group of senior independently + senior_family = [p for p in family if not p.has_next_evolution()] + other_family = [p for p in family if p.has_next_evolution()] + senior_pids = set(p.pokemon_id for p in senior_family) + senior_grouped_family = {pid: [p for p in senior_family if p.pokemon_id == pid] for pid in senior_pids} + + transfer_senior = [] + + for senior_pid, senior_family in senior_grouped_family.items(): + transfer_senior += self.get_family_optimized(senior_pid, senior_family)[0] + + if len(senior_pids) < nb_branch: + # We did not get every combination yet = All other Pokemons are potentially good to keep + evolve_best = other_family + evolve_best.sort(key=lambda p: p.iv * p.ncp, reverse=True) + keep_best = [] + else: + evolve_best = [] + keep_best = [] + + for criteria in self.config_keep: + top = [] + + for f in senior_grouped_family.values(): + top += self.get_top_rank(f, criteria) + + worst = self.get_sorted_family(top, criteria)[-1] + + if criteria.get("evolve", True): + evolve_best += self.get_better_rank(family, criteria, worst) + else: + keep_best += self.get_better_rank(family, criteria, worst) + + evolve_best = self.unique_pokemons(evolve_best) + keep_best = self.unique_pokemons(keep_best) + + transfer, evo_best, evo_crap = self.get_evolution_plan(family_id, other_family, evolve_best, keep_best) + transfer += transfer_senior + + return (transfer, evo_best, evo_crap) + + def get_top_rank(self, family, criteria): + sorted_family = self.get_sorted_family(family, criteria) + worst = sorted_family[criteria.get("top", 1) - 1] + return [p for p in sorted_family if self.get_rank(p, criteria) >= self.get_rank(worst, criteria)] + + def get_better_rank(self, family, criteria, worst): + return [p for p in self.get_sorted_family(family, criteria) if self.get_rank(p, criteria) >= self.get_rank(worst, criteria)] + + def get_sorted_family(self, family, criteria): + return sorted(family, key=lambda p: self.get_rank(p, criteria), reverse=True) + + def get_rank(self, pokemon, criteria): + return tuple(getattr(pokemon, a, None) for a in criteria.get("sort")) + + def get_pokemon_max_cp(self, pokemon_name): + return int(self.pokemon_max_cp.get(pokemon_name, 0)) + + def unique_pokemons(self, l): + seen = set() + return [p for p in l if not (p.id in seen or seen.add(p.id))] + + def get_evolution_plan(self, family_id, family, evolve_best, keep_best): + candies = inventory.candies().get(family_id).quantity + + # All the rest is crap, for now + crap = family[:] + crap = [p for p in crap if p not in evolve_best] + crap = [p for p in crap if p not in keep_best] + crap.sort(key=lambda p: p.iv, reverse=True) + + candies += len(crap) + + # Let's see if we can evolve our best pokemons + can_evolve_best = [] + + for pokemon in evolve_best: + if not pokemon.has_next_evolution(): + continue + + candies -= pokemon.evolution_cost + + if candies < 0: + continue + + can_evolve_best.append(pokemon) + + # Not sure if the evo keep the same id + next_pid = pokemon.next_evolution_ids[0] + next_evo = copy.copy(pokemon) + next_evo.pokemon_id = next_pid + next_evo._static_data = inventory.pokemons().data_for(next_pid) + next_evo.name = inventory.pokemons().name_for(next_pid) + evolve_best.append(next_evo) + + # Compute how many crap we should keep if we want to batch evolve them for xp + junior_evolution_cost = inventory.pokemons().evolution_cost_for(family_id) + + # transfer + keep_for_evo = len(crap) + # leftover_candies = candies - len(crap) + transfer * 1 + # keep_for_evo = leftover_candies / junior_evolution_cost + # + # keep_for_evo = (candies - len(crap) + transfer) / junior_evolution_cost + # keep_for_evo = (candies - keep_for_evo) / junior_evolution_cost + + if (candies > 0) and junior_evolution_cost: + keep_for_evo = int(candies / (junior_evolution_cost + 1)) + else: + keep_for_evo = 0 + + evo_crap = [p for p in crap if p.has_next_evolution() and p.evolution_cost == junior_evolution_cost][:keep_for_evo] + transfer = [p for p in crap if p not in evo_crap] + + return (transfer, can_evolve_best, evo_crap) + + def apply_optimization(self, transfer, evo): + for pokemon in transfer: + self.transfer_pokemon(pokemon) + + if self.config_evolve and self.config_use_lucky_egg: + lucky_egg = inventory.items().get(Item.ITEM_LUCKY_EGG.value) # @UndefinedVariable + + if lucky_egg.count == 0: + if self.config_evolve_only_with_lucky_egg: + self.logger.info("Skipping evolution step. No lucky egg available") + return + elif len(evo) >= self.config_minimum_evolve_for_lucky_egg: + self.use_lucky_egg() + + self.logger.info("Evolving %s Pokemons", len(evo)) + + for pokemon in evo: + self.evolve_pokemon(pokemon) + + def transfer_pokemon(self, pokemon): + if self.config_transfer: + self.bot.api.release_pokemon(pokemon_id=pokemon.id) + else: + pass + + self.emit_event("pokemon_release", + formatted="Exchanged {pokemon} [IV {iv}] [CP {cp}]", + data={"pokemon": pokemon.name, + "iv": pokemon.iv, + "cp": pokemon.cp}) + + if self.config_transfer: + inventory.candies().get(pokemon.pokemon_id).add(1) + action_delay(self.bot.config.action_wait_min, self.bot.config.action_wait_max) + + def use_lucky_egg(self): + lucky_egg = inventory.items().get(Item.ITEM_LUCKY_EGG.value) # @UndefinedVariable + + if self.config_evolve and self.config_use_lucky_egg: + response_dict = self.bot.use_lucky_egg() + lucky_egg.remove(1) + else: + response_dict = {"responses": {"USE_ITEM_XP_BOOST": {"result": 1}}} + + if not response_dict: + self.emit_event("lucky_egg_error", + level='error', + formatted="Failed to use lucky egg!") + return False + + result = response_dict.get("responses", {}).get("USE_ITEM_XP_BOOST", {}).get("result", 0) + + if result == 1: + self.emit_event("used_lucky_egg", + formatted="Used lucky egg ({amount_left} left).", + data={"amount_left": lucky_egg.count}) + return True + else: + self.emit_event("lucky_egg_error", + level='error', + formatted="Failed to use lucky egg!") + return False + + def evolve_pokemon(self, pokemon): + if self.config_evolve: + response_dict = self.bot.api.evolve_pokemon(pokemon_id=pokemon.id) + else: + response_dict = {"responses": {"EVOLVE_POKEMON": {"result": 1}}} + + if not response_dict: + return False + + result = response_dict.get("responses", {}).get("EVOLVE_POKEMON", {}).get("result", 0) + xp = response_dict.get("responses", {}).get("EVOLVE_POKEMON", {}).get("experience_awarded", 0) + + if result == 1: + self.emit_event("pokemon_evolved", + formatted="Evolved {pokemon} [IV {iv}] [CP {cp}] [+{xp} xp]", + data={"pokemon": pokemon.name, + "iv": pokemon.iv, + "cp": pokemon.cp, + "xp": xp}) + + if self.config_evolve: + inventory.candies().get(pokemon.pokemon_id).consume(pokemon.evolution_cost) + sleep(20) + + return True + else: + return False From ac07ad368c3d11d8b2cca2cdd9e18b7395054514 Mon Sep 17 00:00:00 2001 From: Vianney Dubus Date: Fri, 12 Aug 2016 09:44:54 +0200 Subject: [PATCH 123/143] NicknamePokemon: Format iv_pct on 3 digits (#3698) For better sorting on pokemon's name, format iv_pct on 3 digits. --- pokemongo_bot/cell_workers/nickname_pokemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/cell_workers/nickname_pokemon.py b/pokemongo_bot/cell_workers/nickname_pokemon.py index e521cadfba..68943e7a8e 100644 --- a/pokemongo_bot/cell_workers/nickname_pokemon.py +++ b/pokemongo_bot/cell_workers/nickname_pokemon.py @@ -50,7 +50,7 @@ def _nickname_pokemon(self,pokemon): iv_list = [iv_attack,iv_defense,iv_stamina] iv_ads = "/".join(map(str,iv_list)) iv_sum = sum(iv_list) - iv_pct = "{:0.0f}".format(100*iv_sum/45.0) + iv_pct = "{:03.0f}".format(100*iv_sum/45.0) log_color = 'red' try: new_name = self.template.format(name=name, From 50cd7bf243eab90b88a2d8bd99e105b39647f331 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 12 Aug 2016 04:59:57 -0400 Subject: [PATCH 124/143] Config/encrypt fix (#3707) * Fix typo in config * Fix all configs * Fixed __init__.py thanks to @hklcf --- configs/config.json.cluster.example | 2 +- configs/config.json.example | 2 +- configs/config.json.map.example | 2 +- configs/config.json.optimizer.example | 1 + configs/config.json.path.example | 2 +- configs/config.json.pokemon.example | 2 +- pokemongo_bot/__init__.py | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index bb01bb44f3..c9cc3b3ca3 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -4,7 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", - "libencrypt_location": "", + "encrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.example b/configs/config.json.example index 502c18c78e..2cf5a0f737 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -4,7 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", - "libencrypt_location": "", + "encrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.map.example b/configs/config.json.map.example index acb6b2f5ea..ff2b8a69e2 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -4,7 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", - "libencrypt_location": "", + "encrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.optimizer.example b/configs/config.json.optimizer.example index f63a2b7ae1..4db7f2cee6 100644 --- a/configs/config.json.optimizer.example +++ b/configs/config.json.optimizer.example @@ -4,6 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", + "encrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 52286b2809..23578f8125 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -4,7 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", - "libencrypt_location": "", + "encrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index c271f21fcc..a2d5d96a2b 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -4,7 +4,7 @@ "password": "YOUR_PASSWORD", "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", - "libencrypt_location": "", + "encrypt_location": "", "tasks": [ { "type": "HandleSoftBan" diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index f49d698464..7d8a207ece 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -670,7 +670,7 @@ def get_encryption_lib(self): full_path = path + '/'+ file_name if not os.path.isfile(full_path): - self.logger.error(file_name + ' is not found! Please place it in the bots root directory or set libencrypt_location in config.') + self.logger.error(file_name + ' is not found! Please place it in the bots root directory or set encrypt_location in config.') self.logger.info('Platform: '+ _platform + ' Encrypt.so directory: '+ path) sys.exit(1) else: From 62715ae7f9385b4557e304cfc420b25ec34dde62 Mon Sep 17 00:00:00 2001 From: Chris Wild Date: Fri, 12 Aug 2016 14:09:30 +0100 Subject: [PATCH 125/143] Fixed EventManager handlers to be list instead of tuple (#3734) --- pokemongo_bot/event_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemongo_bot/event_manager.py b/pokemongo_bot/event_manager.py index 3773ec8a9e..3d759bf666 100644 --- a/pokemongo_bot/event_manager.py +++ b/pokemongo_bot/event_manager.py @@ -23,7 +23,7 @@ class EventManager(object): def __init__(self, *handlers): self._registered_events = dict() - self._handlers = handlers or [] + self._handlers = list(handlers) or [] def event_report(self): for event, parameters in self._registered_events.iteritems(): From fff1eacec9d59c90bd73c881e86ac07c91345c23 Mon Sep 17 00:00:00 2001 From: nivong Date: Fri, 12 Aug 2016 16:40:53 +0200 Subject: [PATCH 126/143] Heaps of updates to docs and other small errors in running the bot. (#3593) * Update setup.sh * fixed for mac creating encrypt.so * for now just do wget or curl * this is all in the setup.sh * updated instructions to reflect setup.sh changes * Update installation.md * Update CONTRIBUTORS.md * Update setup.sh * Update installation.md * Update installation.md * Update installation.md * added missing submodule update * Update installation.md * Update installation.md * Update installation.md * Delete install.sh * Update .gitignore * Update installation.md * Update setup.sh * Update installation.md * Update run.sh add `source bin/activate` if someone forget to use virtualenv. --- CONTRIBUTORS.md | 2 ++ docs/installation.md | 31 ++++++++++----------- install.sh | 64 -------------------------------------------- run.sh | 7 +++-- setup.sh | 9 +++++-- 5 files changed, 26 insertions(+), 87 deletions(-) delete mode 100755 install.sh diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 725360784b..14a7d95514 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -64,5 +64,7 @@ * cmezh * Nivong * kestel + * simonsmh * joaodragao * extink + diff --git a/docs/installation.md b/docs/installation.md index 873638ff54..b8951f62d2 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -9,21 +9,20 @@ #Linux/Mac Automatic installation ### Easy installation -1. Run setup.sh -e - This will create the needed encrypted.so file -2. Run setup.sh -i - This will install the bot and all stuff that is needed to run it (follow the guide) -3. Run setup.sh -c - This will make the config file needed, only basic stuff is changed here. If you want to edit more edit this file after: config/config.json -4. Run run.sh - This will run the bot and will start leveling your pokemon go account. +1. Clone the git: `git clone https://github.com/PokemonGoF/PokemonGo-Bot` +2. Go into the new directory: `cd PokemonGo-Bot` +3. Run `./setup.sh -i` + This will install the bot and all stuff that is needed to run it (follow the steps in this process) +4. Run `./run.sh` + After you are done following it this will start your bot. ### To update 1. Stop the bot if it's running. (use control + c twice to stop it) -2. Run setup.sh -r +2. Run `./setup.sh -r` This will reset and makes sure you have no changes made to any code since it will overide it -3. Run setup.sh -u - This will run git pull and will update to the new git update. +3. Rerun the bot `./run.sh` + +note: we do not support windows at this time # Manual installation @@ -31,15 +30,13 @@ - OS X: `brew update && brew install --devel protobuf` - Windows: Download protobuf 3.0: [here](https://github.com/google/protobuf/releases/download/v3.0.0-beta-4/protoc-3.0.0-beta-4-win32.zip) and unzip `bin/protoc.exe` into a folder in your PATH. -- Linux: `apt-get install python-protobuf` ### Get encrypt.so (Windows part writing need fine tune) -We don't have the copyright of encrypt.so, please grab from internet and build your self.Take the risk as your own. -Example build sequence: -Create a new separate folder some here +Due to copywrite on the encrypt.so we are not directly hosting it. Please find a copy elsewhere on the internet and compile it yourself. We accept no responsibility should you encounter any problems with files you download elsewhere. + +Ensure you are in the PokemonGo-Bot main folder and run: -wget http://pgoapi.com/pgoencrypt.tar.gz && tar -xf pgoencrypt.tar.gz && cd pgoencrypt/src/ && make -Then copy libencrypt.so to the gofbot folder and rename to encrypt.so +`wget http://pgoapi.com/pgoencrypt.tar.gz && tar -xf pgoencrypt.tar.gz && cd pgoencrypt/src/ && make && mv libencrypt.so ../../encrypt.so && cd ../..` ### Note on branch Please keep in mind that master is not always up-to-date whereas 'dev' is. In the installation note below change `master` to `dev` if you want to get and use the latest version. diff --git a/install.sh b/install.sh deleted file mode 100755 index f11e58e136..0000000000 --- a/install.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -pokebotpath=$(cd "$(dirname "$0")"; pwd) -cd $pokebotpath -if [ -f /etc/debian_version ] -then -echo "You are on Debian/Ubuntu" -sudo apt-get update -sudo apt-get -y install python python-pip python-dev build-essential git virtualenv -elif [ -f /etc/redhat-release ] -then -echo "You are on CentOS/RedHat" -sudo yum -y install epel-release -sudo yum -y install python-pip -elif [ "$(uname -s)" == "Darwin" ] -then -echo "You are on Mac os" -sudo brew update -sudo brew install --devel protobuf -else -echo "Please check if you have python pip protobuf gcc make installed on your device." -echo "Wait 5 seconds to continue or Use ctrl+c to interrupt this shell." -sleep 5 -fi -pip install virtualenv -cd $pokebotpath -git pull -git submodule init -git submodule foreach git pull origin master -virtualenv . -source bin/activate -pip install -r requirements.txt -echo "Start to make encrypt.so" -wget http://pgoapi.com/pgoencrypt.tar.gz -tar -xf pgoencrypt.tar.gz -cd pgoencrypt/src/ -make -mv libencrypt.so $pokebotpath/encrypt.so -cd ../.. -rm -rf pgoencrypt.tar.gz -rm -rf pgoencrypt -echo "Install complete. Starting to generate config.json." -cd $pokebotpath -read -p "1.google 2.ptc -" auth -read -p "Input username -" username -read -p "Input password -" -s password -read -p " -Input location -" location -read -p "Input gmapkey -" gmapkey -cp configs/config.json.example configs/config.json -if [ "$auth" = "2" ] -then -sed -i "s/google/ptc/g" configs/config.json -fi -sed -i "s/YOUR_USERNAME/$username/g" configs/config.json -sed -i "s/YOUR_PASSWORD/$password/g" configs/config.json -sed -i "s/SOME_LOCATION/$location/g" configs/config.json -sed -i "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" configs/config.json -echo "Edit configs/config.json to modify any other config. Use run.sh ./configs/config.json to run." -exit 0 diff --git a/run.sh b/run.sh index 9938f8c5e0..fa6d8ffab1 100755 --- a/run.sh +++ b/run.sh @@ -6,14 +6,13 @@ filename=$1 else filename="./configs/config.json" fi - +cd $pokebotpath +source bin/activate if [ ! -f "$filename" ]; then -echo "There's no "$filename" file. use setup.sh -config to creat one." +echo "There's no "$filename" file. Please use ./setup.sh -c to creat one." fi - while true do -cd $pokebotpath python pokecli.py -cf $filename echo `date`" Pokebot "$*" Stopped." read -p "Press any button or wait 20 seconds to continue. diff --git a/setup.sh b/setup.sh index c9ec2a1c5e..9535bc7d10 100755 --- a/setup.sh +++ b/setup.sh @@ -1,11 +1,12 @@ #!/usr/bin/env bash +#encoding=utf8 pokebotpath=$(cd "$(dirname "$0")"; pwd) backuppath=$pokebotpath"/backup" function Pokebotupdate () { cd $pokebotpath git pull -git submodule init +git submodule update --init --recursive git submodule foreach git pull origin master virtualenv . source bin/activate @@ -14,7 +15,11 @@ pip install -r requirements.txt function Pokebotencrypt () { echo "Start to make encrypt.so" -wget http://pgoapi.com/pgoencrypt.tar.gz +if [ "$(uname -s)" == "Darwin" ]; then #Mac platform + curl -O http://pgoapi.com/pgoencrypt.tar.gz +elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then #GNU/Linux platform + wget http://pgoapi.com/pgoencrypt.tar.gz +fi tar -xf pgoencrypt.tar.gz cd pgoencrypt/src/ make From ee9c6f3a1f22797c83dd34ce6f70cf4b810d1fcc Mon Sep 17 00:00:00 2001 From: Douglas Camata Date: Fri, 12 Aug 2016 17:19:59 +0200 Subject: [PATCH 127/143] Update docker.md --- docs/docker.md | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/docs/docker.md b/docs/docker.md index 2b2181bb56..11f8634744 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,42 +1,48 @@ -Start by downloading for your platform: [Mac](https://www.docker.com/products/docker#/mac), [Windows](https://www.docker.com/products/docker#/windows), or [Linux](https://www.docker.com/products/docker#/linux). Once you have Docker installed, simply create the various config.json files for your different accounts (e.g. `configs/config-account1.json`) and then create a Docker image for PokemonGo-Bot using the Dockerfile in this [repo](https://hub.docker.com/r/svlentink/pokemongo-bot/). +Start by downloading for your platform: [Mac](https://www.docker.com/products/docker#/mac), [Windows](https://www.docker.com/products/docker#/windows), or [Linux](https://www.docker.com/products/docker#/linux). -#Setup -##Automatic setup -Use this docker hub url: https://hub.docker.com/r/svlentink/pokemongo-bot/ -``` -docker pull svlentink/pokemongo-bot -``` +Once you have Docker installed, simply create the various config files for your different accounts (e.g. `configs/config.json`, `configs/userdata.js`) and then create a Docker image for PokemonGo-Bot using the Dockerfile in this repo. -##Manual setup ``` cd PokemonGo-Bot -docker build -t pokemongo-bot . +docker build --build-arg timezone=Europe/London -t pokemongo-bot . ``` -#Run +Optionally you can set your timezone with the --build-arg option (default is Etc/UTC) + +After build process you can verify that the image was created with: -You can verify that the image was created with: ``` docker images ``` -- In case of automatic setup, you'll see an image called: `svlentink/pokemongo-bot` -- In case of manual setup, you'll see an image called: `pokemongo-bot` -To run PokemonGo-Bot Docker image you've created, simple run: +To run PokemonGo-Bot Docker image you've created: + ``` -docker run --name=pokego-bot1 --rm -it -v $(pwd)/configs/config-account1.json:/usr/src/app/configs/config.json -v $(pwd)/data:/usr/src/app/data pokemongo-bot +docker run --name=bot1-pokego --rm -it -v $(pwd)/configs/config.json:/usr/src/app/configs/config.json pokemongo-bot ``` -Replace `pokemongo-bot` with `svlentink/pokemongo-bot` in case you followed automatic setup. +Run a second container provided with the OpenPoGoBotWeb view: -_Check the logs in real-time `docker logs -f pgobot`_ +``` +docker run --name=bot1-pokegoweb --rm -it --volumes-from bot1-pokego -p 8000:8000 -v $(pwd)/configs/userdata.js:/usr/src/app/web/userdata.js -w /usr/src/app/web python:2.7 python -m SimpleHTTPServer +``` +The OpenPoGoWeb will be served on `http://:8000` -If you want to run multiple accounts with the same Docker image, simply specify different config.json and names in the Docker run command. -Do not push your image to a registry with your config.json and account details in it! +if docker-compose [installed](https://docs.docker.com/compose/install/) you can alternatively run the PokemonGo-Bot ecosystem with one simple command: +(by using the docker-compose.yml configuration in this repo) -Share web folder with host: ``` -docker run -it -v $(pwd)/web/:/usr/src/app/web --rm --name=pgo-bot-acct1 pokemongo-bot --config config.json +docker-compose up ``` -TODO: Add configuration for running multiple Docker containers from the same image for every bot instance, and a single container for the web UI. +Also run one single service from the compose configuration is possible: + +``` +docker-compose run --rm bot1-pokego +``` + +command for remove all stopped containers: `docker-compose rm` + +TODO: Add infos / configuration for running multiple bot instances. + +Do not push your image to a registry with your config.json and account details in it! From 6f626fa1c29d33b3c8769063cfe8a5996735258a Mon Sep 17 00:00:00 2001 From: Dmitry Ovodov Date: Sat, 13 Aug 2016 00:15:54 +0700 Subject: [PATCH 128/143] Modify pokemon_catch_worker.py to use Inventory class and fix #3411 (#3496) * Fix #3411. Update inventory info before every catch try otherwise old values used * Revert "Fix #3411. Update inventory info before every catch try otherwise old values used" This reverts commit f7678da0f68573a7397c4c55a9804ee22dcbd53e. * Modify pokemon_catch_worker.py to use Inventory class * Fix forgotten line * Fix one more forgotten line * Added check if we really used berry or not * Fix KeyError in inventory.py When we have no items of type, there are no "count" key in the dict. * Revert "Fix KeyError in inventory.py" This reverts commit ed2769c51820381044332f9e95e759bda6dc587e. * Revert "Added check if we really used berry or not" This reverts commit 42e9d9cc2c0335da0bed2adabc797f154ecc1596. * Revert "Fix one more forgotten line" This reverts commit 5fda3c49ea6483ad16bf2582dee0aed14ed34b6b. * Revert "Fix forgotten line" This reverts commit a8edc5723a1b88334beead9cd863291b894a0a49. * Revert "Modify pokemon_catch_worker.py to use Inventory class" This reverts commit 5b6e4d39bccbae6a825274080727bd7ef62b6d98. * Modify pokemon_catch_worker.py to use Inventory class and fix #3411 --- .../cell_workers/pokemon_catch_worker.py | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index d28f3f58fd..27d762caef 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -54,8 +54,7 @@ def __init__(self, pokemon, bot): self.position = bot.position self.config = bot.config self.pokemon_list = bot.pokemon_list - self.item_list = bot.item_list - self.inventory = bot.inventory + self.inventory = inventory.items() self.spawn_point_guid = '' self.response_key = '' self.response_status_key = '' @@ -204,8 +203,8 @@ def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, cu formatted='Catch rate of {catch_rate} with {ball_name} is low. Throwing {berry_name} (have {berry_count})', data={ 'catch_rate': self._pct(catch_rate_by_ball[current_ball]), - 'ball_name': self.item_list[str(current_ball)], - 'berry_name': self.item_list[str(berry_id)], + 'ball_name': self.inventory.get(current_ball).name, + 'berry_name': self.inventory.get(berry_id).name, 'berry_count': berry_count } ) @@ -227,8 +226,8 @@ def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, cu 'threw_berry', formatted="Threw a {berry_name}! Catch rate with {ball_name} is now: {new_catch_rate}", data={ - 'berry_name': self.item_list[str(berry_id)], - 'ball_name': self.item_list[str(current_ball)], + 'berry_name': self.inventory.get(berry_id).name, + 'ball_name': self.inventory.get(current_ball).name, 'new_catch_rate': self._pct(new_catch_rate_by_ball[current_ball]) } ) @@ -261,16 +260,18 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): maximum_ball = ITEM_ULTRABALL if is_vip else ITEM_GREATBALL ideal_catch_rate_before_throw = 0.9 if is_vip else 0.35 - berry_count = self.bot.item_inventory_count(berry_id) - items_stock = self.bot.current_inventory() + berry_count = self.inventory.get(ITEM_RAZZBERRY).count + ball_count = {} + for ball_id in [ITEM_POKEBALL, ITEM_GREATBALL, ITEM_ULTRABALL]: + ball_count[ball_id] = self.inventory.get(ball_id).count while True: # find lowest available ball current_ball = ITEM_POKEBALL - while items_stock[current_ball] == 0 and current_ball < maximum_ball: + while ball_count[current_ball] == 0 and current_ball < maximum_ball: current_ball += 1 - if items_stock[current_ball] == 0: + if ball_count[current_ball] == 0: self.emit_event('no_pokeballs', formatted='No usable pokeballs found!') break @@ -279,7 +280,7 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): next_ball = current_ball while next_ball < maximum_ball: next_ball += 1 - num_next_balls += items_stock[next_ball] + num_next_balls += ball_count[next_ball] # check if we've got berries to spare berries_to_spare = berry_count > 0 if is_vip else berry_count > num_next_balls + 30 @@ -287,23 +288,29 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): # use a berry if we are under our ideal rate and have berries to spare used_berry = False if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and berries_to_spare: - catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) - berry_count -= 1 - used_berry = True + new_catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) + if new_catch_rate_by_ball != catch_rate_by_ball: + catch_rate_by_ball = new_catch_rate_by_ball + self.inventory.get(ITEM_RAZZBERRY).remove(1) + berry_count -= 1 + used_berry = True # pick the best ball to catch with best_ball = current_ball while best_ball < maximum_ball: best_ball += 1 - if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and items_stock[best_ball] > 0: + if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and ball_count[best_ball] > 0: # if current ball chance to catch is under our ideal rate, and player has better ball - then use it current_ball = best_ball # if the rate is still low and we didn't throw a berry before, throw one if catch_rate_by_ball[current_ball] < ideal_catch_rate_before_throw and berry_count > 0 and not used_berry: - catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) - berry_count -= 1 - + new_catch_rate_by_ball = self._use_berry(berry_id, berry_count, encounter_id, catch_rate_by_ball, current_ball) + if new_catch_rate_by_ball != catch_rate_by_ball: + catch_rate_by_ball = new_catch_rate_by_ball + self.inventory.get(ITEM_RAZZBERRY).remove(1) + berry_count -= 1 + # Randomize the quality of the throw # Default structure throw_parameters = {'normalized_reticle_size': 1.950, @@ -315,14 +322,15 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): # try to catch pokemon! # TODO : Log which type of throw we selected - items_stock[current_ball] -= 1 + ball_count[current_ball] -= 1 + self.inventory.get(current_ball).remove(1) self.emit_event( 'threw_pokeball', formatted='Used {ball_name}, with chance {success_percentage} ({count_left} left)', data={ - 'ball_name': self.item_list[str(current_ball)], + 'ball_name': self.inventory.get(current_ball).name, 'success_percentage': self._pct(catch_rate_by_ball[current_ball]), - 'count_left': items_stock[current_ball] + 'count_left': ball_count[current_ball] } ) From 59d08651ba157be2a13de586ebd760c5c4be23ea Mon Sep 17 00:00:00 2001 From: joaodragao Date: Fri, 12 Aug 2016 18:17:46 +0100 Subject: [PATCH 129/143] Use Ultraball If No Other Balls (With Constraint) (#3421) * Add Use Ultraball (#1) * Add `use_ultraball` event to Event Manager * Add use ultraball if pokeball + greatball = 0 * Add Use Ultraball if No Other Balls (#2) * Add `use_ultraball` event to Event Manager * Add use ultraball if pokeball + greatball = 0 * Add New Contributor * Revert "Add Use Ultraball" (#4) * Use Ultraball If No Other Balls (#3) * Add `use_ultraball` event to Event Manager * Add use ultraball if pokeball + greatball = 0 * Add New Contributor * Remove 'use_ultraball' event. * Remove `use_ultraball` event call * Update & add avoid catching Pokemon if no pokeball * Update conflict contributors * Add get `min_ultraball_to_keep` from config file * Improved `min_ultraball_to_keep` with condition * Added `min_ultraball_to_keep` option * Added `min_ultraball_to_keep` option * Added `min_ultraball_to_keep` option * Added `min_ultraball_to_keep` option * Add `min_ultraball_to_keep` option * Remove count all pokeballs * Resolved Conflicts --- configs/config.json.cluster.example | 1 + configs/config.json.example | 3 +++ configs/config.json.map.example | 1 + configs/config.json.path.example | 1 + configs/config.json.pokemon.example | 1 + pokecli.py | 1 + pokemongo_bot/cell_workers/pokemon_catch_worker.py | 14 +++++++++++++- 7 files changed, 21 insertions(+), 1 deletion(-) diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index c9cc3b3ca3..1bedbcbab9 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -100,6 +100,7 @@ "reconnecting_timeout": 15, "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "min_ultraball_to_keep": 10, "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, diff --git a/configs/config.json.example b/configs/config.json.example index 2cf5a0f737..f306d3a920 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -113,6 +113,9 @@ "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, + "catch_randomize_reticle_factor": 1.0, + "catch_randomize_spin_factor": 1.0, + "min_ultraball_to_keep": 10, "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, diff --git a/configs/config.json.map.example b/configs/config.json.map.example index ff2b8a69e2..6051e063cc 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -342,6 +342,7 @@ "reconnecting_timeout": 15, "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "min_ultraball_to_keep": 10, "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 23578f8125..2581862b31 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -102,6 +102,7 @@ "reconnecting_timeout": 15, "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "min_ultraball_to_keep": 10, "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index a2d5d96a2b..2ad81a7369 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -108,6 +108,7 @@ "reconnecting_timeout": 15, "catch_randomize_reticle_factor": 1.0, "catch_randomize_spin_factor": 1.0, + "min_ultraball_to_keep": 10, "logging_color": true, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or" }, diff --git a/pokecli.py b/pokecli.py index 2cd5553ba5..4ccedb5fa6 100644 --- a/pokecli.py +++ b/pokecli.py @@ -454,6 +454,7 @@ def _json_loader(filename): config.action_wait_min = load.get('action_wait_min', 1) config.plugins = load.get('plugins', []) config.raw_tasks = load.get('tasks', []) + config.min_ultraball_to_keep = load.get('min_ultraball_to_keep', None) config.vips = load.get('vips', {}) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 27d762caef..c950b5bac4 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -265,6 +265,12 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): for ball_id in [ITEM_POKEBALL, ITEM_GREATBALL, ITEM_ULTRABALL]: ball_count[ball_id] = self.inventory.get(ball_id).count + # use `min_ultraball_to_keep` from config if is not None + min_ultraball_to_keep = items_stock[ITEM_ULTRABALL] + if self.config.min_ultraball_to_keep is not None: + if self.config.min_ultraball_to_keep >= 0 and self.config.min_ultraball_to_keep < min_ultraball_to_keep: + min_ultraball_to_keep = self.config.min_ultraball_to_keep + while True: # find lowest available ball @@ -273,7 +279,13 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): current_ball += 1 if ball_count[current_ball] == 0: self.emit_event('no_pokeballs', formatted='No usable pokeballs found!') - break + + # use untraball if there is no other balls with constraint to `min_ultraball_to_keep` + if maximum_ball != ITEM_ULTRABALL and items_stock[ITEM_ULTRABALL] > min_ultraball_to_keep: + maximum_ball = ITEM_ULTRABALL + continue + else: + break # check future ball count num_next_balls = 0 From 76587deac79ead429312243845d78d7978c4d6b8 Mon Sep 17 00:00:00 2001 From: achretien Date: Fri, 12 Aug 2016 19:30:43 +0200 Subject: [PATCH 130/143] Add and Remove pokemon from the inventory cache when catch, release and evolve (#3738) * Add and Remove pokemon from the inventory cache when catch and release * Add dealing with evolved pokemon also * Add the evolved pokemon --- pokemongo_bot/cell_workers/evolve_pokemon.py | 7 +++- .../cell_workers/pokemon_catch_worker.py | 34 ++++++------------- .../cell_workers/transfer_pokemon.py | 8 ++++- pokemongo_bot/inventory.py | 18 +++++++++- 4 files changed, 41 insertions(+), 26 deletions(-) diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index 3f764f9d58..b5675aba8f 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -1,5 +1,6 @@ from pokemongo_bot import inventory from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.inventory import Pokemon from pokemongo_bot.item_list import Item from pokemongo_bot.base_task import BaseTask @@ -110,7 +111,11 @@ def _execute_pokemon_evolve(self, pokemon, cache): 'xp': 0 } ) - inventory.candies().get(pokemon.pokemon_id).consume(pokemon.evolution_cost) + awarded_candies = response_dict.get('responses', {}).get('EVOLVE_POKEMON', {}).get('candy_awarded', 0) + inventory.candies().get(pokemon.pokemon_id).consume(pokemon.evolution_cost - awarded_candies) + inventory.pokemons().remove(pokemon.id) + pokemon = Pokemon(response_dict.get('responses', {}).get('EVOLVE_POKEMON', {}).get('evolved_pokemon_data', {})) + inventory.pokemons().add(pokemon) sleep(self.evolve_speed) return True else: diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index c950b5bac4..1e5b161d13 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -5,6 +5,7 @@ from pokemongo_bot import inventory from pokemongo_bot.base_task import BaseTask from pokemongo_bot.human_behaviour import sleep +from pokemongo_bot.inventory import Pokemon from pokemongo_bot.worker_result import WorkerResult CATCH_STATUS_SUCCESS = 1 @@ -26,25 +27,6 @@ } -class Pokemon(object): - - def __init__(self, pokemon_list, pokemon_data): - self.num = int(pokemon_data['pokemon_id']) - self.name = pokemon_list[int(self.num) - 1]['Name'] - self.cp = pokemon_data['cp'] - self.attack = pokemon_data.get('individual_attack', 0) - self.defense = pokemon_data.get('individual_defense', 0) - self.stamina = pokemon_data.get('individual_stamina', 0) - - @property - def iv(self): - return round((self.attack + self.defense + self.stamina) / 45.0, 2) - - @property - def iv_display(self): - return '{}/{}/{}'.format(self.attack, self.defense, self.stamina) - - class PokemonCatchWorker(BaseTask): def __init__(self, pokemon, bot): @@ -84,7 +66,7 @@ def work(self, response_dict=None): # get pokemon data pokemon_data = response['wild_pokemon']['pokemon_data'] if 'wild_pokemon' in response else response['pokemon_data'] - pokemon = Pokemon(self.pokemon_list, pokemon_data) + pokemon = Pokemon(pokemon_data) # skip ignored pokemon if not self._should_catch_pokemon(pokemon): @@ -102,7 +84,7 @@ def work(self, response_dict=None): 'encounter_id': self.pokemon['encounter_id'], 'latitude': self.pokemon['latitude'], 'longitude': self.pokemon['longitude'], - 'pokemon_id': pokemon.num + 'pokemon_id': pokemon.pokemon_id } ) @@ -256,6 +238,10 @@ def _use_berry(self, berry_id, berry_count, encounter_id, catch_rate_by_ball, cu def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): # settings that may be exposed at some point + """ + + :type pokemon: Pokemon + """ berry_id = ITEM_RAZZBERRY maximum_ball = ITEM_ULTRABALL if is_vip else ITEM_GREATBALL ideal_catch_rate_before_throw = 0.9 if is_vip else 0.35 @@ -389,7 +375,9 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): # pokemon caught! elif catch_pokemon_status == CATCH_STATUS_SUCCESS: + pokemon.id = response_dict['responses']['CATCH_POKEMON']['captured_pokemon_id'] self.bot.metrics.captured_pokemon(pokemon.name, pokemon.cp, pokemon.iv_display, pokemon.iv) + inventory.pokemons().add(pokemon) self.emit_event( 'pokemon_caught', formatted='Captured {pokemon}! [CP {cp}] [Potential {iv}] [{iv_display}] [+{exp} exp]', @@ -402,12 +390,12 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): 'encounter_id': self.pokemon['encounter_id'], 'latitude': self.pokemon['latitude'], 'longitude': self.pokemon['longitude'], - 'pokemon_id': pokemon.num + 'pokemon_id': pokemon.pokemon_id } ) # We could refresh here too, but adding 3 saves a inventory request - candy = inventory.candies(True).get(pokemon.num) + candy = inventory.candies(True).get(pokemon.pokemon_id) self.emit_event( 'gained_candy', formatted='You now have {quantity} {type} candy!', diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 9e970d7d7f..a8eb62c34a 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -4,7 +4,7 @@ from pokemongo_bot import inventory from pokemongo_bot.human_behaviour import action_delay from pokemongo_bot.base_task import BaseTask -from pokemongo_bot.inventory import Pokemons +from pokemongo_bot.inventory import Pokemons, Pokemon class TransferPokemon(BaseTask): @@ -66,6 +66,7 @@ def work(self): def _release_pokemon_get_groups(self): pokemon_groups = {} + # TODO: Use new inventory everywhere and then remove the inventory update for pokemon in inventory.pokemons(True).all(): if pokemon.in_fort or pokemon.is_favorite: continue @@ -134,6 +135,10 @@ def should_release_pokemon(self, pokemon, keep_best_mode = False): return logic_to_function[cp_iv_logic](*release_results.values()) def release_pokemon(self, pokemon): + """ + + :type pokemon: Pokemon + """ try: if self.bot.config.test: candy_awarded = 1 @@ -146,6 +151,7 @@ def release_pokemon(self, pokemon): # We could refresh here too, but adding 1 saves a inventory request candy = inventory.candies().get(pokemon.pokemon_id) candy.add(candy_awarded) + inventory.pokemons().remove(pokemon.id) self.bot.metrics.released_pokemon() self.emit_event( 'pokemon_release', diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index 3ade99c0af..c7c3dfafcd 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -341,6 +341,18 @@ def all(self): # makes caller's lives more difficult) return [p for p in super(Pokemons, self).all() if not isinstance(p, Egg)] + def add(self, pokemon): + if pokemon.id <= 0: + raise ValueError("Can't add a pokemin whitout id") + if pokemon.id in self._data: + raise ValueError("Pokemon already present in the inventory") + self._data[pokemon.id] = pokemon + + def remove(self, pokemon_id): + if pokemon_id not in self._data: + raise ValueError("Pokemon not present in the inventory") + self._data.pop(pokemon_id) + # # Static Components @@ -484,7 +496,7 @@ class Pokemon(object): def __init__(self, data): self._data = data # Unique ID for this particular Pokemon - self.id = data['id'] + self.id = data.get('id', 0) # Id of the such pokemons in pokedex self.pokemon_id = data['pokemon_id'] @@ -597,6 +609,10 @@ def candy_quantity(self): def evolution_cost(self): return Pokemons.evolution_cost_for(self.pokemon_id) + @property + def iv_display(self): + return '{}/{}/{}'.format(self.iv_attack, self.iv_defense, self.iv_stamina) + def _compute_iv_perfection(self): total_iv = self.iv_attack + self.iv_defense + self.iv_stamina iv_perfection = round((total_iv / 45.0), 2) From d580dfe39781f2590812815bf1c000b6d173cad7 Mon Sep 17 00:00:00 2001 From: PLG Date: Fri, 12 Aug 2016 19:45:10 +0200 Subject: [PATCH 131/143] Update configuration_files.md (#3742) MoveToMapPokemon behavior related to issue #3736 (discussed with @k4n30) --- docs/configuration_files.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/configuration_files.md b/docs/configuration_files.md index a881740b9a..ce9f5078a6 100644 --- a/docs/configuration_files.md +++ b/docs/configuration_files.md @@ -238,7 +238,9 @@ This task will fetch current pokemon spawns from /raw_data of an PokemonGo-Map i * `update_map` - disable/enable if the map location should be automatically updated to the bots current location * `catch` - A dictionary of pokemon to catch with an assigned priority (higher => better) * `snipe_high_prio_only` - Whether to snipe pokemon above a certain threshold. -* `snipe_high_prio_threshold` - The threshold number corresponding with the `catch` dictionary. Any pokemon above this threshold will be caught. Other will be igonored. +* `snipe_high_prio_threshold` - The threshold number corresponding with the `catch` dictionary. +* - Any pokemon above this threshold value will be caught by teleporting to its location, and getting back to original location if `snipe` is `True`. +* - Any pokemon under this threshold value will make the bot walk to the Pokemon target wether `snipe` is `True` or `False`. #### Example ``` From 12b47d367814d4d38ef3ca2cc3ae039f9ac96525 Mon Sep 17 00:00:00 2001 From: Quantra Date: Fri, 12 Aug 2016 18:45:56 +0100 Subject: [PATCH 132/143] Cache recent forts (for forts.max_circle_size) (#3556) * added bool option to cache recent forts -crf --forts.cache_recent_forts (default true) saves recent_forts in data/recent-forts-{username}.json on spin loads recent_forts from same file on start up bot doesn't start a new recent_forts on every reset * forgot contributor * typo fix no_cached_forts * changed all events related to caching forts to debug level * caching of forts happens on sigterm/exception handling of SIGTERM -Note handling of SIGTERM in python2.7 with multi threads is not reliable. Child thread can recieve SIGTERM and it is not handled in pokecli.py; pokecli.py continues to run. --- CONTRIBUTORS.md | 1 + configs/config.json.example | 3 +- pokecli.py | 44 ++++++++++++++++++++- pokemongo_bot/__init__.py | 52 +++++++++++++++++++++++++ pokemongo_bot/cell_workers/spin_fort.py | 3 ++ 5 files changed, 101 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 14a7d95514..1e463bb8d3 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -67,4 +67,5 @@ * simonsmh * joaodragao * extink + * Quantra diff --git a/configs/config.json.example b/configs/config.json.example index f306d3a920..838acc1d4b 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -101,7 +101,8 @@ "map_object_cache_time": 5, "forts": { "avoid_circles": true, - "max_circle_size": 50 + "max_circle_size": 50, + "cache_recent_forts": true }, "websocket_server": false, "walk": 4.16, diff --git a/pokecli.py b/pokecli.py index 4ccedb5fa6..ef50212d9f 100644 --- a/pokecli.py +++ b/pokecli.py @@ -33,6 +33,7 @@ import ssl import sys import time +import signal from datetime import timedelta from getpass import getpass from pgoapi.exceptions import NotLoggedInException, ServerSideRequestThrottlingException, ServerBusyOrOfflineException @@ -58,10 +59,16 @@ logger = logging.getLogger('cli') logger.setLevel(logging.INFO) +class SIGINTRecieved(Exception): pass + def main(): bot = False try: + def handle_sigint(*args): + raise SIGINTRecieved + signal.signal(signal.SIGINT, handle_sigint) + logger.info('PokemonGO Bot v1.0') sys.stdout = codecs.getwriter('utf8')(sys.stdout) sys.stderr = codecs.getwriter('utf8')(sys.stderr) @@ -95,7 +102,7 @@ def main(): while True: bot.tick() - except KeyboardInterrupt: + except (KeyboardInterrupt, SIGINTRecieved): bot.event_manager.emit( 'bot_exit', sender=bot, @@ -138,6 +145,32 @@ def main(): report_summary(bot) raise + finally: + # Cache here on SIGTERM, or Exception. Check data is available and worth caching. + if bot: + if bot.recent_forts[-1] is not None and bot.config.forts_cache_recent_forts: + cached_forts_path = os.path.join( + _base_dir, 'data', 'recent-forts-%s.json' % bot.config.username + ) + try: + with open(cached_forts_path, 'w') as outfile: + json.dump(bot.recent_forts, outfile) + bot.event_manager.emit( + 'cached_fort', + sender=bot, + level='debug', + formatted='Forts cached.', + ) + except IOError as e: + bot.event_manager.emit( + 'error_caching_forts', + sender=bot, + level='debug', + formatted='Error caching forts for {path}', + data={'path': cached_forts_path} + ) + + def report_summary(bot): if bot.metrics.start_time is None: @@ -362,6 +395,15 @@ def _json_loader(filename): type=int, default=10, ) + add_config( + parser, + load, + short_flag="-crf", + long_flag="--forts.cache_recent_forts", + help="Caches recent forts used by max_circle_size", + type=bool, + default=True, + ) add_config( parser, load, diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 7d8a207ece..dcf83bdc84 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -84,6 +84,7 @@ def start(self): self._setup_event_system() self._setup_logging() self._setup_api() + self._load_recent_forts() random.seed() @@ -456,6 +457,18 @@ def _register_events(self): parameters=('last_lat', 'last_lon') ) + # cached recent_forts + self.event_manager.register_event('loaded_cached_forts') + self.event_manager.register_event('cached_fort') + self.event_manager.register_event( + 'no_cached_forts', + parameters=('path', ) + ) + self.event_manager.register_event( + 'error_caching_forts', + parameters=('path', ) + ) + def tick(self): self.health_record.heartbeat() self.cell = self.get_meta_cell() @@ -1093,3 +1106,42 @@ def get_map_objects(self, lat, lng, timestamp, cellid): self.last_time_map_object = time.time() return self.last_map_object + + def _load_recent_forts(self): + if not self.config.forts_cache_recent_forts: + return + + + cached_forts_path = os.path.join(_base_dir, 'data', 'recent-forts-%s.json' % self.config.username) + try: + # load the cached recent forts + with open(cached_forts_path) as f: + cached_recent_forts = json.load(f) + + num_cached_recent_forts = len(cached_recent_forts) + num_recent_forts = len(self.recent_forts) + + # Handles changes in max_circle_size + if not num_recent_forts: + self.recent_forts = [] + elif num_recent_forts > num_cached_recent_forts: + self.recent_forts[-num_cached_recent_forts:] = cached_recent_forts + elif num_recent_forts < num_cached_recent_forts: + self.recent_forts = cached_recent_forts[-num_recent_forts:] + else: + self.recent_forts = cached_recent_forts + + self.event_manager.emit( + 'loaded_cached_forts', + sender=self, + level='debug', + formatted='Loaded cached forts...' + ) + except IOError: + self.event_manager.emit( + 'no_cached_forts', + sender=self, + level='debug', + formatted='Starting new cached forts for {path}', + data={'path': cached_forts_path} + ) diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 61d3eb02bd..0ba09ca633 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import json +import os import time from pgoapi.utilities import f2i @@ -9,6 +11,7 @@ from pokemongo_bot.human_behaviour import sleep from pokemongo_bot.worker_result import WorkerResult from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.base_dir import _base_dir from utils import distance, format_time, fort_details From 4b632d84c2c4422ee0cd0342b2b882e8c1bce2bb Mon Sep 17 00:00:00 2001 From: joaodragao Date: Fri, 12 Aug 2016 18:47:13 +0100 Subject: [PATCH 133/143] Update use ultraball with constraint (#3760) * Add Use Ultraball (#1) * Add `use_ultraball` event to Event Manager * Add use ultraball if pokeball + greatball = 0 * Add Use Ultraball if No Other Balls (#2) * Add `use_ultraball` event to Event Manager * Add use ultraball if pokeball + greatball = 0 * Add New Contributor * Revert "Add Use Ultraball" (#4) * Use Ultraball If No Other Balls (#3) * Add `use_ultraball` event to Event Manager * Add use ultraball if pokeball + greatball = 0 * Add New Contributor * Remove 'use_ultraball' event. * Remove `use_ultraball` event call * Update & add avoid catching Pokemon if no pokeball * Update conflict contributors * Add get `min_ultraball_to_keep` from config file * Improved `min_ultraball_to_keep` with condition * Added `min_ultraball_to_keep` option * Added `min_ultraball_to_keep` option * Added `min_ultraball_to_keep` option * Added `min_ultraball_to_keep` option * Add `min_ultraball_to_keep` option * Remove count all pokeballs * Resolved Conflicts * Change from `items_stock` to `ball_count` --- pokemongo_bot/cell_workers/pokemon_catch_worker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 1e5b161d13..4030a33e41 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -252,7 +252,7 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): ball_count[ball_id] = self.inventory.get(ball_id).count # use `min_ultraball_to_keep` from config if is not None - min_ultraball_to_keep = items_stock[ITEM_ULTRABALL] + min_ultraball_to_keep = ball_count[ITEM_ULTRABALL] if self.config.min_ultraball_to_keep is not None: if self.config.min_ultraball_to_keep >= 0 and self.config.min_ultraball_to_keep < min_ultraball_to_keep: min_ultraball_to_keep = self.config.min_ultraball_to_keep @@ -267,7 +267,7 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): self.emit_event('no_pokeballs', formatted='No usable pokeballs found!') # use untraball if there is no other balls with constraint to `min_ultraball_to_keep` - if maximum_ball != ITEM_ULTRABALL and items_stock[ITEM_ULTRABALL] > min_ultraball_to_keep: + if maximum_ball != ITEM_ULTRABALL and ball_count[ITEM_ULTRABALL] > min_ultraball_to_keep: maximum_ball = ITEM_ULTRABALL continue else: From a50ca97a9bea5e1160170a67630db068dfac97f3 Mon Sep 17 00:00:00 2001 From: Amal Samally Date: Fri, 12 Aug 2016 21:55:41 +0400 Subject: [PATCH 134/143] Rewrite NicknamePokemon for new Inventory sysem + a lot of new keys for 'nickname_template' option (#3756) * Add type information and classes - New classes: Type, Types - Usage of new classes anywhere in the Inventory - Tests coverage * Improve API for pokemons in Inventory - Added new class PokemonInfo for the static information loaded from json - API improved, added capture_rate and flee_rate - All covered with tests * Minor refactoring of inventory.py - Item class (moved to other instance classes) to keep file structure - Code style * Rewrite NicknamePokemon to use new Inventory sysem + add a lot of new keys for 'nickname_template' option + tests & documentation * Update documentaion for the new NicknamePokemon * Update documentaion for the new NicknamePokemon (again :) --- data/types.json | 247 +++++++ docs/configuration_files.md | 55 +- pokemongo_bot/__init__.py | 12 +- .../cell_workers/nickname_pokemon.py | 446 +++++++++++-- pokemongo_bot/inventory.py | 613 ++++++++++++------ tests/inventory_test.py | 110 ++-- tests/nickname_test.py | 91 +++ 7 files changed, 1258 insertions(+), 316 deletions(-) create mode 100644 data/types.json create mode 100644 tests/nickname_test.py diff --git a/data/types.json b/data/types.json new file mode 100644 index 0000000000..b4bad5e297 --- /dev/null +++ b/data/types.json @@ -0,0 +1,247 @@ +[ + { + "name": "Bug", + "effectiveAgainst": [ + "Dark", + "Grass", + "Psychic" + ], + "weakAgainst": [ + "Fairy", + "Fighting", + "Fire", + "Flying", + "Ghost", + "Poison", + "Steel" + ] + }, + { + "name": "Dark", + "effectiveAgainst": [ + "Ghost", + "Psychic" + ], + "weakAgainst": [ + "Dark", + "Fairy", + "Fighting" + ] + }, + { + "name": "Dragon", + "effectiveAgainst": [ + "Dragon" + ], + "weakAgainst": [ + "Fairy", + "Steel" + ] + }, + { + "name": "Electric", + "effectiveAgainst": [ + "Flying", + "Water" + ], + "weakAgainst": [ + "Dragon", + "Electric", + "Grass", + "Ground" + ] + }, + { + "name": "Fairy", + "effectiveAgainst": [ + "Dark", + "Dragon", + "Fighting" + ], + "weakAgainst": [ + "Fire", + "Poison", + "Steel" + ] + }, + { + "name": "Fighting", + "effectiveAgainst": [ + "Dark", + "Ice", + "Normal", + "Rock", + "Steel" + ], + "weakAgainst": [ + "Bug", + "Fairy", + "Flying", + "Ghost", + "Poison", + "Psychic" + ] + }, + { + "name": "Fire", + "effectiveAgainst": [ + "Bug", + "Grass", + "Ice", + "Steel" + ], + "weakAgainst": [ + "Dragon", + "Fire", + "Rock", + "Water" + ] + }, + { + "name": "Flying", + "effectiveAgainst": [ + "Bug", + "Fighting", + "Grass" + ], + "weakAgainst": [ + "Electric", + "Rock", + "Steel" + ] + }, + { + "name": "Ghost", + "effectiveAgainst": [ + "Ghost", + "Psychic" + ], + "weakAgainst": [ + "Dark", + "Normal" + ] + }, + { + "name": "Grass", + "effectiveAgainst": [ + "Ground", + "Rock", + "Water" + ], + "weakAgainst": [ + "Bug", + "Dragon", + "Fire", + "Flying", + "Grass", + "Poison", + "Steel" + ] + }, + { + "name": "Ground", + "effectiveAgainst": [ + "Electric", + "Fire", + "Poison", + "Rock", + "Steel" + ], + "weakAgainst": [ + "Bug", + "Flying", + "Grass" + ] + }, + { + "name": "Ice", + "effectiveAgainst": [ + "Dragon", + "Flying", + "Grass", + "Ground" + ], + "weakAgainst": [ + "Fire", + "Ice", + "Steel", + "Water" + ] + }, + { + "name": "Normal", + "effectiveAgainst": [], + "weakAgainst": [ + "Ghost", + "Rock", + "Steel" + ] + }, + { + "name": "Poison", + "effectiveAgainst": [ + "Fairy", + "Grass" + ], + "weakAgainst": [ + "Ghost", + "Ground", + "Poison", + "Rock", + "Steel" + ] + }, + { + "name": "Psychic", + "effectiveAgainst": [ + "Fighting", + "Poison" + ], + "weakAgainst": [ + "Dark", + "Psychic", + "Steel" + ] + }, + { + "name": "Rock", + "effectiveAgainst": [ + "Bug", + "Fire", + "Flying", + "Ice" + ], + "weakAgainst": [ + "Fighting", + "Ground", + "Steel" + ] + }, + { + "name": "Steel", + "effectiveAgainst": [ + "Fairy", + "Ice", + "Rock" + ], + "weakAgainst": [ + "Electric", + "Fire", + "Steel", + "Water" + ] + }, + { + "name": "Water", + "effectiveAgainst": [ + "Fire", + "Ground", + "Rock" + ], + "weakAgainst": [ + "Dragon", + "Grass", + "Water" + ] + } +] diff --git a/docs/configuration_files.md b/docs/configuration_files.md index ce9f5078a6..42b4feb069 100644 --- a/docs/configuration_files.md +++ b/docs/configuration_files.md @@ -40,6 +40,8 @@ The behaviors of the bot are configured via the `tasks` key in the `config.json` * [MoveToMapPokemon](#sniping-movetolocation) * NicknamePokemon * `nickname_template`: Default `""` | See the [Pokemon Nicknaming](#pokemon-nicknaming) section for more details + * `dont_nickname_favorite`: Default `false` | Prevents renaming of favorited pokemons + * `good_attack_threshold`: Default `0.7` | Threshold for perfection of the attack in it's type *(0.0-1.0)* after which attack will be treated as good.
Used for `{fast_attack_char}`, `{charged_attack_char}`, `{attack_code}` templates * RecycleItems * `item_filter`: Pass a list of unwanted [items (using their JSON codes)](https://github.com/PokemonGoF/PokemonGo-Bot/wiki/Item-ID's) to recycle when collected at a Pokestop * SpinFort @@ -202,15 +204,49 @@ Niantic imposes a 12-character limit on all pokemon nicknames, so any new nickna Because some pokemon have very long names, you can use the [Format String syntax](https://docs.python.org/2.7/library/string.html#formatstrings) to ensure that your names do not cause your templates to truncate. For example, using `{name:.8s}` causes the Pokemon name to never take up more than 8 characters in the nickname. This would help guarantee that a template like `{name:.8s}_{iv_pct}` never goes over the 12-character limit. Valid names in templates are: -- `name` = pokemon name -- `id` = pokemon type id (e.g. 1 for Bulbasaurs) -- `cp` = pokemon's CP -- `iv_attack` = pokemon's attack IV -- `iv_defense` = pokemon's defense IV -- `iv_stamina` = pokemon's stamina IV -- `iv_ads` = pokemon's IVs in `(attack)/(defense)/(stamina)` format (matches web UI format -- A/D/S) -- `iv_sum` = pokemon's IVs as a sum (e.g. 45 when 3 perfect 15 IVs) -- `iv_pct` = pokemon's IVs as a percentage (0-100) + +Key | Info +---- | ---- +**{name}** | Pokemon name *(e.g. Articuno)* +**{id}** | Pokemon ID/Number *(1-151, e.g. 1 for Bulbasaurs)* +**{cp}** | Pokemon's Combat Points (CP) *(10-4145)* + | **Individial Values (IV)** +**{iv_attack}** | Individial Attack *(0-15)* of the current specific pokemon +**{iv_defense}** | Individial Defense *(0-15)* of the current specific pokemon +**{iv_stamina}** | Individial Stamina *(0-15)* of the current specific pokemon +**{iv_ads}** | Joined IV values in `(attack)/(defense)/(stamina)` format (*e.g. 4/12/9*, matches web UI format -- A/D/S) +**{iv_sum}** | Sum of the Individial Values *(0-45, e.g. 45 when 3 perfect 15 IVs)* + | **Basic Values of the pokemon (identical for all of one kind)** +**{base_attack}** | Basic Attack *(40-284)* of the current pokemon kind +**{base_defense}** | Basic Defense *(54-242)* of the current pokemon kind +**{base_stamina}** | Basic Stamina *(20-500)* of the current pokemon kind +**{base_ads}** | Joined Basic Values *(e.g. 125/93/314)* + | **Final Values of the pokemon (Base Values + Individial Values)** +**{attack}** | Basic Attack + Individial Attack +**{defense}** | Basic Defense + Individial Defense +**{stamina}** | Basic Stamina + Individial Stamina +**{sum_ads}** | Joined Final Values *(e.g. 129/97/321)* + | **Individial Values perfection percent** +**{iv_pct}** | IV perfection *(in 000-100 format - 3 chars)* +**{iv_pct2}** | IV perfection *(in 00-99 format - 2 chars).* So 99 is best (it's a 100% perfection) +**{iv_pct1}** | IV perfection *(in 0-9 format - 1 char)* + | **IV CP perfection - kind of IV perfection percent but calculated using weight of each IV in its contribution to CP of the best evolution of current pokemon.**
It tends to be more accurate than simple IV perfection. +**{ivcp_pct}** | IV CP perfection *(in 000-100 format - 3 chars)* +**{ivcp_pct2}** | IV CP perfection *(in 00-99 format - 2 chars).* So 99 is best (it's a 100% perfection) +**{ivcp_pct1}** | IV CP perfection *(in 0-9 format - 1 char)* + | **Moveset perfection percents for attack and for defense.**
Calculated for current pokemon only, not between all pokemons. So perfect moveset can be weak if pokemon is weak (e.g. Caterpie) +**{attack_pct}** | Moveset perfection for attack *(in 000-100 format - 3 chars)* +**{attack_pct2}** | Moveset perfection for attack *(in 00-99 format - 2 chars)* +**{attack_pct1}** | Moveset perfection for attack *(in 0-9 format - 1 char)* +**{defense_pct}** | Moveset perfection for defense *(in 000-100 format - 3 chars)* +**{defense_pct2}** | Moveset perfection for defense *(in 00-99 format - 2 chars)* +**{defense_pct1}** | Moveset perfection for defense *(in 0-9 format - 1 char)* + | **Character codes for fast/charged attack types.**
If attack is good character is uppecased, otherwise lowercased.
Use `'good_attack_threshold'` option for customization.

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

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