From 629358e1ff596fa6dd5fdbc734442f666de9be76 Mon Sep 17 00:00:00 2001 From: rawgni Date: Wed, 17 Aug 2016 18:51:54 +0700 Subject: [PATCH 1/3] fix setup.sh failing on macosx --- setup.sh | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/setup.sh b/setup.sh index 023ba4904a..0b3de84b27 100755 --- a/setup.sh +++ b/setup.sh @@ -43,15 +43,10 @@ Input location " location read -p "Input gmapkey " gmapkey -cp -f configs/config.json.example configs/config.json && chmod 755 configs/config.json -if [ "$auth" = "2" ] || [ "$auth" = "ptc" ] -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 +[[ $auth = "2" || $auth = "ptc" ]] && auth="ptc" || auth="google" +sed -e "s/YOUR_USERNAME/$username/g" -e "s/YOUR_PASSWORD/$password/g" \ + -e "s/SOME_LOCATION/$location/g" -e "s/GOOGLE_MAPS_API_KEY/$gmapkey/g" \ + -e "s/google/$auth/g" configs/config.json.example > configs/config.json echo "Edit ./configs/config.json to modify any other config." } From f2df44fdbd7197b7732b5dd6e4e4667037bcc700 Mon Sep 17 00:00:00 2001 From: Ingwar Wirjawan Date: Wed, 17 Aug 2016 22:22:53 +0700 Subject: [PATCH 2/3] Update CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index db121e8e46..284ed59435 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -71,3 +71,4 @@ * pmquan * net8q * SyncX + * rawgni From 3f1b4521145719f0bc5c76fd147e1698ebe19ba2 Mon Sep 17 00:00:00 2001 From: rawgni Date: Thu, 18 Aug 2016 21:36:46 +0700 Subject: [PATCH 3/3] merge upstream --- .gitignore | 1 + .travis.yml | 1 + CONTRIBUTORS.md | 1 + configs/config.json.cluster.example | 44 ++++- configs/config.json.example | 108 +++++----- configs/config.json.map.example | 66 +++++-- configs/config.json.optimizer.example | 184 +++++++++++------- configs/config.json.path.example | 102 +++++----- configs/config.json.pokemon.example | 43 +++- docs/configuration_files.md | 16 ++ docs/installation.md | 10 +- json-validate.py | 44 +++++ pokecli.py | 2 +- pokemongo_bot/__init__.py | 48 ++++- pokemongo_bot/api_wrapper.py | 2 +- pokemongo_bot/cell_workers/__init__.py | 2 + pokemongo_bot/cell_workers/catch_pokemon.py | 7 + .../cell_workers/complete_tutorial.py | 136 +++++++++++++ .../cell_workers/migrations/catch_log.py | 5 + .../cell_workers/move_to_map_pokemon.py | 11 +- .../cell_workers/pokemon_catch_worker.py | 153 ++++++++++----- pokemongo_bot/cell_workers/random_pause.py | 133 +++++++++++++ .../cell_workers/transfer_pokemon.py | 54 ++++- .../cell_workers/update_live_stats.py | 2 + pokemongo_bot/datastore.py | 65 +++++++ .../event_handlers/colored_logging_handler.py | 3 + pokemongo_bot/inventory.py | 7 +- pokemongo_bot/migrations/pokemongobot.py | 5 + requirements.txt | 3 +- 29 files changed, 989 insertions(+), 269 deletions(-) create mode 100644 json-validate.py create mode 100644 pokemongo_bot/cell_workers/complete_tutorial.py create mode 100644 pokemongo_bot/cell_workers/migrations/catch_log.py create mode 100644 pokemongo_bot/cell_workers/random_pause.py create mode 100644 pokemongo_bot/datastore.py create mode 100644 pokemongo_bot/migrations/pokemongobot.py diff --git a/.gitignore b/.gitignore index 550648c7b6..0f969ba7cc 100644 --- a/.gitignore +++ b/.gitignore @@ -106,6 +106,7 @@ out/ # Personal load details src/ web/ +data/*.db data/last-location*.json data/cells-*.json data/map-caught-*.json diff --git a/.travis.yml b/.travis.yml index aef60fa3c7..0aab748960 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,4 +12,5 @@ install: - pip install pylint script: - python pylint-recursive.py + - python json-validate.py configs/*.json.* - python -m unittest discover -v -p "*_test.py" diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 284ed59435..e33ffd327c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -71,4 +71,5 @@ * pmquan * net8q * SyncX + * umbreon222 * rawgni diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index 194d6c7e5c..d8759c06c7 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -5,6 +5,8 @@ "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", "encrypt_location": "", + "websocket_server": false, + "heartbeat_threshold": 10, "tasks": [ { "type": "HandleSoftBan" @@ -20,7 +22,15 @@ } }, { - "type": "CollectLevelUpReward" + "type": "CompleteTutorial", + "config": { + "enabled": false, + "// set a name": "", + "nickname": "" + } + }, + { + "type": "CollectLevelUpReward" }, { "type": "IncubateEggs", @@ -39,7 +49,18 @@ } }, { - "type": "TransferPokemon" + "type": "TransferPokemon", + "config": { + "transfer_wait_min": 1, + "transfer_wait_max": 4 + } + }, + { + "type": "NicknamePokemon", + "config": { + "enabled": false, + "nickname_template": "{iv_pct}_{iv_ads}" + } }, { "type": "EvolvePokemon", @@ -57,6 +78,10 @@ "type": "RecycleItems", "config": { "min_empty_space": 15, + "max_balls_keep": 150, + "max_potions_keep": 50, + "max_berries_keep": 70, + "max_revives_keep": 70, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, @@ -64,7 +89,9 @@ "Hyper Potion": { "keep" : 30 }, "Revive": { "keep" : 30 }, "Razz Berry": { "keep" : 100 } - } + }, + "recycle_wait_min": 1, + "recycle_wait_max": 4 } }, { @@ -89,11 +116,11 @@ "berry_wait_max": 3, "changeball_wait_min": 2, "changeball_wait_max": 3 - }, + } } }, { - "type": "SpinFort" + "type": "SpinFort", "config": { "spin_wait_min": 2, "spin_wait_max": 3 @@ -109,13 +136,11 @@ ], "forts": { "avoid_circles": true, - "max_circle_size": 50 + "max_circle_size": 50, + "cache_recent_forts": true }, - "websocket_server": false, "walk_max": 4.16, "walk_min": 2.16, - "action_wait_min": 1, - "action_wait_max": 4, "debug": false, "test": false, "health_record": true, @@ -123,6 +148,7 @@ "distance_unit": "km", "reconnecting_timeout": 15, "logging_color": true, + "daily_catch_limit": 800, "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 842e15a687..0bbc6c1623 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -20,9 +20,27 @@ "time_random_offset": "00:24", "duration_random_offset": "00:43" } + }, + { + "type": "RandomPause", + "config": { + "enabled": false, + "min_duration": "00:00:10", + "max_duration": "00:10:00", + "min_interval": "00:10:00", + "max_interval": "02:00:00" + } + }, + { + "type": "CompleteTutorial", + "config": { + "enabled": false, + "// set a name": "", + "nickname": "" + } }, { - "type": "CollectLevelUpReward" + "type": "CollectLevelUpReward" }, { "type": "IncubateEggs", @@ -41,10 +59,10 @@ } }, { - "type": "TransferPokemon" + "type": "TransferPokemon", "config": { - "transfer_wait_min": 1, - "transfer_wait_max": 4 + "transfer_wait_min": 1, + "transfer_wait_max": 4 } }, { @@ -57,13 +75,13 @@ { "type": "EvolvePokemon", "config": { - "evolve_all": "none", - "first_evolve_by": "cp", - "evolve_above_cp": 500, - "evolve_above_iv": 0.8, - "logic": "or", - "evolve_speed": 20, - "use_lucky_egg": false + "evolve_all": "none", + "first_evolve_by": "cp", + "evolve_above_cp": 500, + "evolve_above_iv": 0.8, + "logic": "or", + "evolve_speed": 20, + "use_lucky_egg": false } }, { @@ -81,38 +99,38 @@ "Hyper Potion": { "keep" : 30 }, "Revive": { "keep" : 30 }, "Razz Berry": { "keep" : 100 } - } + }, "recycle_wait_min": 1, "recycle_wait_max": 4 } }, - { - "type": "CatchPokemon", - "config": { - "catch_visible_pokemon": true, - "catch_lured_pokemon": true, - "min_ultraball_to_keep": 5, - "catch_throw_parameters": { - "excellent_rate": 0.1, - "great_rate": 0.5, - "nice_rate": 0.3, - "normal_rate": 0.1, - "spin_success_rate" : 0.6 - }, - "catch_simulation": { - "flee_count": 3, - "flee_duration": 2, - "catch_wait_min": 2, - "catch_wait_max": 6, - "berry_wait_min": 2, - "berry_wait_max": 3, - "changeball_wait_min": 2, - "changeball_wait_max": 3 - }, - } - }, { - "type": "SpinFort" + "type": "CatchPokemon", + "config": { + "catch_visible_pokemon": true, + "catch_lured_pokemon": true, + "min_ultraball_to_keep": 5, + "catch_throw_parameters": { + "excellent_rate": 0.1, + "great_rate": 0.5, + "nice_rate": 0.3, + "normal_rate": 0.1, + "spin_success_rate" : 0.6 + }, + "catch_simulation": { + "flee_count": 3, + "flee_duration": 2, + "catch_wait_min": 2, + "catch_wait_max": 6, + "berry_wait_min": 2, + "berry_wait_max": 3, + "changeball_wait_min": 2, + "changeball_wait_max": 3 + } + } + }, + { + "type": "SpinFort", "config": { "spin_wait_min": 2, "spin_wait_max": 3 @@ -121,8 +139,8 @@ { "type": "MoveToFort", "config": { - "lure_attraction": true, - "lure_max_distance": 2000 + "lure_attraction": true, + "lure_max_distance": 2000 } }, { @@ -148,6 +166,7 @@ "distance_unit": "km", "reconnecting_timeout": 15, "logging_color": true, + "daily_catch_limit": 800, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, @@ -159,15 +178,17 @@ "// Rattata": {"always_release": true}, "// Example of keeping 3 stronger (based on CP) Pidgey:": {}, "// Pidgey": {"keep_best_cp": 3}, - "// Example of keeping 2 stronger (based on IV) Zubat:": {}, + "// Example of keeping 2 best (based on IV) Zubat:": {}, "// Zubat": {"keep_best_iv": 2}, "// Also, it is working with any": {}, "// any": {"keep_best_iv": 3}, "// Example of keeping the 2 strongest (based on CP) and 3 best (based on IV) Zubat:": {}, - "// Zubat": {"keep_best_cp": 2, "keep_best_iv": 3} + "// Zubat": {"keep_best_cp": 2, "keep_best_iv": 3}, + "// Example of custom order of static criterion": {}, + "// Zubat": {"keep_best_custom": "moveset.defense_perfection, hp_max", "amount":2} }, "vips" : { - "Any pokemon put here directly force to use Berry & Best Ball to capture, to secure the capture rate!": {}, + "Any pokemon put here directly force to use Berry & Best Ball to capture, to secure the capture rate!": {}, "any": {"catch_above_cp": 1200, "catch_above_iv": 0.9, "logic": "or" }, "Lapras": {}, "Moltres": {}, @@ -187,6 +208,5 @@ "Muk": {}, "Weezing": {}, "Flareon": {} - } } diff --git a/configs/config.json.map.example b/configs/config.json.map.example index 1bd393f591..27ca31383c 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -5,6 +5,7 @@ "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", "encrypt_location": "", + "websocket_server": false, "heartbeat_threshold": 10, "tasks": [ { @@ -21,7 +22,15 @@ } }, { - "type": "CollectLevelUpReward" + "type": "CompleteTutorial", + "config": { + "enabled": false, + "// set a name": "", + "nickname": "" + } + }, + { + "type": "CollectLevelUpReward" }, { "type": "IncubateEggs", @@ -40,21 +49,39 @@ } }, { - "type": "TransferPokemon" + "type": "TransferPokemon", + "config": { + "transfer_wait_min": 1, + "transfer_wait_max": 4 + } + }, + { + "type": "NicknamePokemon", + "config": { + "enabled": false, + "nickname_template": "{iv_pct}_{iv_ads}" + } }, { "type": "EvolvePokemon", "config": { - "evolve_all": "NONE", - "evolve_cp_min": 300, - "evolve_speed": 20, - "use_lucky_egg": false + "evolve_all": "none", + "first_evolve_by": "cp", + "evolve_above_cp": 500, + "evolve_above_iv": 0.8, + "logic": "or", + "evolve_speed": 20, + "use_lucky_egg": false } }, { "type": "RecycleItems", "config": { "min_empty_space": 15, + "max_balls_keep": 150, + "max_potions_keep": 50, + "max_berries_keep": 70, + "max_revives_keep": 70, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, @@ -62,7 +89,9 @@ "Hyper Potion": { "keep" : 30 }, "Revive": { "keep" : 30 }, "Razz Berry": { "keep" : 100 } - } + }, + "recycle_wait_min": 1, + "recycle_wait_max": 4 } }, { @@ -87,11 +116,11 @@ "berry_wait_max": 3, "changeball_wait_min": 2, "changeball_wait_max": 3 - }, + } } }, { - "type": "SpinFort" + "type": "SpinFort", "config": { "spin_wait_min": 2, "spin_wait_max": 3 @@ -342,22 +371,28 @@ } }, { - "type": "MoveToFort" + "type": "MoveToFort", + "config": { + "lure_attraction": true, + "lure_max_distance": 2000 + } }, { - "type": "FollowSpiral" + "type": "FollowSpiral", + "config": { + "diameter": 4, + "step_size": 70 + } } ], "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_max": 4.16, "walk_min": 2.16, - "action_wait_min": 1, - "action_wait_max": 4, "debug": false, "test": false, "health_record": true, @@ -365,6 +400,7 @@ "distance_unit": "km", "reconnecting_timeout": 15, "logging_color": true, + "daily_catch_limit": 800, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or"}, "// Example of always catching Rattata:": {}, diff --git a/configs/config.json.optimizer.example b/configs/config.json.optimizer.example index 031914b7c3..75fc069cee 100644 --- a/configs/config.json.optimizer.example +++ b/configs/config.json.optimizer.example @@ -5,19 +5,49 @@ "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", "encrypt_location": "", + "websocket_server": false, + "heartbeat_threshold": 10, "tasks": [ - { - "type": "HandleSoftBan" - }, - { - "type": "CollectLevelUpReward" - }, - { - "type": "IncubateEggs", - "config": { - "longer_eggs_first": true - } - }, + { + "type": "HandleSoftBan" + }, + { + "type": "SleepSchedule", + "config": { + "enabled": false, + "time": "22:54", + "duration":"7:46", + "time_random_offset": "00:24", + "duration_random_offset": "00:43" + } + }, + { + "type": "CompleteTutorial", + "config": { + "enabled": false, + "// set a name": "", + "nickname": "" + } + }, + { + "type": "CollectLevelUpReward" + }, + { + "type": "IncubateEggs", + "config": { + "longer_eggs_first": true + } + }, + { + "type": "UpdateLiveStats", + "config": { + "enabled": false, + "min_interval": 10, + "stats": ["username", "uptime", "stardust_earned", "xp_earned", "xp_per_hour", "stops_visited"], + "terminal_log": true, + "terminal_title": true + } + }, { "type": "PokemonOptimizer", "config": { @@ -27,7 +57,7 @@ "// would have transfered if the parameter was true": {}, "transfer": true, "// 'transfer_wait_min' and 'transfer_wait_max' are the minimum and maximum": {}, - "// "time to wait when transferring a pokemon": {}, + "// time to wait when transferring a pokemon": {}, "transfer_wait_min": 1, "transfer_wait_max": 4, "// the 'evolve' parameter activate or deactivate the evolution of pokemons": {}, @@ -101,80 +131,90 @@ } ] } - }, - { - "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": "CatchPokemon", - "config": { - "catch_visible_pokemon": true, - "catch_lured_pokemon": true, - "min_ultraball_to_keep": 5, - "catch_throw_parameters": { - "excellent_rate": 0.1, - "great_rate": 0.5, - "nice_rate": 0.3, - "normal_rate": 0.1, - "spin_success_rate" : 0.6 - }, - "catch_simulation": { - "flee_count": 3, - "flee_duration": 2, - "catch_wait_min": 2, - "catch_wait_max": 6, - "berry_wait_min": 2, - "berry_wait_max": 3, - "changeball_wait_min": 2, - "changeball_wait_max": 3 - }, - } - }, + }, + { + "type": "RecycleItems", + "config": { + "min_empty_space": 15, + "max_balls_keep": 150, + "max_potions_keep": 50, + "max_berries_keep": 70, + "max_revives_keep": 70, + "item_filter": { + "Pokeball": { "keep" : 100 }, + "Potion": { "keep" : 10 }, + "Super Potion": { "keep" : 20 }, + "Hyper Potion": { "keep" : 30 }, + "Revive": { "keep" : 30 }, + "Razz Berry": { "keep" : 100 } + }, + "recycle_wait_min": 1, + "recycle_wait_max": 4 + } + }, { - "type": "SpinFort", - "config": { - "ignore_item_count": true, - "spin_wait_min": 2, - "spin_wait_max": 3 - } + "type": "CatchPokemon", + "config": { + "catch_visible_pokemon": true, + "catch_lured_pokemon": true, + "min_ultraball_to_keep": 5, + "catch_throw_parameters": { + "excellent_rate": 0.1, + "great_rate": 0.5, + "nice_rate": 0.3, + "normal_rate": 0.1, + "spin_success_rate" : 0.6 + }, + "catch_simulation": { + "flee_count": 3, + "flee_duration": 2, + "catch_wait_min": 2, + "catch_wait_max": 6, + "berry_wait_min": 2, + "berry_wait_max": 3, + "changeball_wait_min": 2, + "changeball_wait_max": 3 + } + } }, - { - "type": "MoveToFort", - "config": { - "lure_attraction": true, - "lure_max_distance": 2000, - "ignore_item_count": true - } + { + "type": "SpinFort", + "config": { + "spin_wait_min": 2, + "spin_wait_max": 3 + } + }, + { + "type": "MoveToFort", + "config": { + "lure_attraction": true, + "lure_max_distance": 2000 + } + }, + { + "type": "FollowSpiral", + "config": { + "diameter": 4, + "step_size": 70 } + } ], "map_object_cache_time": 5, "forts": { - "avoid_circles": true, - "max_circle_size": 50 + "avoid_circles": true, + "max_circle_size": 50, + "cache_recent_forts": true }, - "websocket_server": true, "walk_max": 4.16, "walk_min": 2.16, - "action_wait_min": 1, - "action_wait_max": 4, "debug": false, "test": false, - "health_record": false, + "health_record": true, "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, "logging_color": true, + "daily_catch_limit": 800, "catch": { "any": { "always_catch": true diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 9587c66fd6..160ca438df 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -5,6 +5,8 @@ "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", "encrypt_location": "", + "websocket_server": false, + "heartbeat_threshold": 10, "tasks": [ { "type": "HandleSoftBan" @@ -20,7 +22,15 @@ } }, { - "type": "CollectLevelUpReward" + "type": "CompleteTutorial", + "config": { + "enabled": false, + "// set a name": "", + "nickname": "" + } + }, + { + "type": "CollectLevelUpReward" }, { "type": "IncubateEggs", @@ -33,13 +43,24 @@ "config": { "enabled": false, "min_interval": 10, - "stats": ["uptime", "stardust_earned", "xp_earned", "xp_per_hour", "stops_visited"], + "stats": ["username", "uptime", "stardust_earned", "xp_earned", "xp_per_hour", "stops_visited"], "terminal_log": true, "terminal_title": true } }, { - "type": "TransferPokemon" + "type": "TransferPokemon", + "config": { + "transfer_wait_min": 1, + "transfer_wait_max": 4 + } + }, + { + "type": "NicknamePokemon", + "config": { + "enabled": false, + "nickname_template": "{iv_pct}_{iv_ads}" + } }, { "type": "EvolvePokemon", @@ -57,6 +78,10 @@ "type": "RecycleItems", "config": { "min_empty_space": 15, + "max_balls_keep": 150, + "max_potions_keep": 50, + "max_berries_keep": 70, + "max_revives_keep": 70, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, @@ -64,36 +89,38 @@ "Hyper Potion": { "keep" : 30 }, "Revive": { "keep" : 30 }, "Razz Berry": { "keep" : 100 } + }, + "recycle_wait_min": 1, + "recycle_wait_max": 4 + } + }, + { + "type": "CatchPokemon", + "config": { + "catch_visible_pokemon": true, + "catch_lured_pokemon": true, + "min_ultraball_to_keep": 5, + "catch_throw_parameters": { + "excellent_rate": 0.1, + "great_rate": 0.5, + "nice_rate": 0.3, + "normal_rate": 0.1, + "spin_success_rate" : 0.6 + }, + "catch_simulation": { + "flee_count": 3, + "flee_duration": 2, + "catch_wait_min": 2, + "catch_wait_max": 6, + "berry_wait_min": 2, + "berry_wait_max": 3, + "changeball_wait_min": 2, + "changeball_wait_max": 3 } } }, - { - "type": "CatchPokemon", - "config": { - "catch_visible_pokemon": true, - "catch_lured_pokemon": true, - "min_ultraball_to_keep": 5, - "catch_throw_parameters": { - "excellent_rate": 0.1, - "great_rate": 0.5, - "nice_rate": 0.3, - "normal_rate": 0.1, - "spin_success_rate" : 0.6 - }, - "catch_simulation": { - "flee_count": 3, - "flee_duration": 2, - "catch_wait_min": 2, - "catch_wait_max": 6, - "berry_wait_min": 2, - "berry_wait_max": 3, - "changeball_wait_min": 2, - "changeball_wait_max": 3 - }, - } - }, { - "type": "SpinFort" + "type": "SpinFort", "config": { "spin_wait_min": 2, "spin_wait_max": 3 @@ -113,31 +140,16 @@ "avoid_circles": true, "max_circle_size": 50 }, - "websocket_server": false, "walk_max": 4.16, "walk_min": 2.16, - "action_wait_min": 1, - "action_wait_max": 4, "debug": false, "test": false, "health_record": true, "location_cache": true, "distance_unit": "km", "reconnecting_timeout": 15, - "catch_randomize_reticle_factor": 1.0, - "catch_randomize_spin_factor": 1.0, - "catch_simulation": { - "flee_count": 3, - "flee_duration": 2, - "catch_wait_min": 2, - "catch_wait_max": 6, - "berry_wait_min": 2, - "berry_wait_max": 3, - "changeball_wait_min": 2, - "changeball_wait_max": 3 - }, - "min_ultraball_to_keep": 10, "logging_color": true, + "daily_catch_limit": 800, "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 9aa7eebe88..06f554e021 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -5,6 +5,7 @@ "location": "SOME_LOCATION", "gmapkey": "GOOGLE_MAPS_API_KEY", "encrypt_location": "", + "websocket_server": false, "heartbeat_threshold": 10, "tasks": [ { @@ -21,7 +22,15 @@ } }, { - "type": "CollectLevelUpReward" + "type": "CompleteTutorial", + "config": { + "enabled": false, + "// set a name": "", + "nickname": "" + } + }, + { + "type": "CollectLevelUpReward" }, { "type": "IncubateEggs", @@ -40,7 +49,18 @@ } }, { - "type": "TransferPokemon" + "type": "TransferPokemon", + "config": { + "transfer_wait_min": 1, + "transfer_wait_max": 4 + } + }, + { + "type": "NicknamePokemon", + "config": { + "enabled": false, + "nickname_template": "{iv_pct}_{iv_ads}" + } }, { "type": "EvolvePokemon", @@ -58,6 +78,10 @@ "type": "RecycleItems", "config": { "min_empty_space": 15, + "max_balls_keep": 150, + "max_potions_keep": 50, + "max_berries_keep": 70, + "max_revives_keep": 70, "item_filter": { "Pokeball": { "keep" : 100 }, "Potion": { "keep" : 10 }, @@ -65,7 +89,9 @@ "Hyper Potion": { "keep" : 30 }, "Revive": { "keep" : 30 }, "Razz Berry": { "keep" : 100 } - } + }, + "recycle_wait_min": 1, + "recycle_wait_max": 4 } }, { @@ -90,11 +116,11 @@ "berry_wait_max": 3, "changeball_wait_min": 2, "changeball_wait_max": 3 - }, + } } }, { - "type": "SpinFort" + "type": "SpinFort", "config": { "spin_wait_min": 2, "spin_wait_max": 3 @@ -118,13 +144,11 @@ "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_max": 4.16, "walk_min": 2.16, - "action_wait_min": 1, - "action_wait_max": 4, "debug": false, "test": false, "health_record": true, @@ -132,6 +156,7 @@ "distance_unit": "km", "reconnecting_timeout": 15, "logging_color": true, + "daily_catch_limit": 800, "catch": { "any": {"catch_above_cp": 0, "catch_above_iv": 0, "logic": "or" }, diff --git a/docs/configuration_files.md b/docs/configuration_files.md index 706904e44d..f2b9f42b79 100644 --- a/docs/configuration_files.md +++ b/docs/configuration_files.md @@ -19,6 +19,7 @@ | `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 +|`daily_catch_llimit` | 800 | Limit the amount of pokemon caught in a 24 hour period. ## 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). @@ -162,6 +163,21 @@ If you already have it, it will keep a stronger version and will transfer the a ```"release": {"any": {"keep_best_cp": 2}}```, ```"release": {"any": {"keep_best_cp": 10}}``` - can be any number. +### Keep the best custom pokemon configuration (dev branch) + +Define a list of criteria to keep the best Pokemons according to those criteria. + +The list of criteria is the following:```'cp','iv', 'iv_attack', 'iv_defense', 'iv_stamina', 'moveset.attack_perfection', 'moveset.defense_perfection', 'hp', 'hp_max'``` + +####Examples: + +- Keep the top 25 Zubat with the best hp_max: + +```"release": {"Zubat": {"keep_best_custom": "hp_max", "amount":25}}``` +- Keep the top 10 Zubat with the best hp_max and, if there are Zubat with the same hp_max, to keep the one with the highest hp: + +```"release": {"Zubat": {"keep_best_custom": "hp_max,hp", "amount":10}}```` + ## Evolve All Configuration By setting the `evolve_all` attribute in config.json, you can instruct the bot to automatically diff --git a/docs/installation.md b/docs/installation.md index c1e7361522..36f34bfb88 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -133,13 +133,21 @@ if docker-compose [installed](https://docs.docker.com/compose/install/) you can docker-compose up ``` +An example of routing the bot's traffic through a tor proxy can be found within the docker-compose_tor.yml file. To use a different file, supply the file name to docker-compose. The d flag is used to run this in detached mode as the tor logs overwhelm any bot logs you might wish to view. The bot logs can still be seen through `docker logs` command. + +``` +docker-compose -f docker-compose_tor.yml up -d +``` + 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` + + +command to stop and remove all stopped containers: `docker-compose down` TODO: Add infos / configuration for running multiple bot instances. diff --git a/json-validate.py b/json-validate.py new file mode 100644 index 0000000000..00061ba256 --- /dev/null +++ b/json-validate.py @@ -0,0 +1,44 @@ +#! /usr/bin/env python +''' +Check whether a json file is loadable +''' + +import json +import sys + +passed = 0 +failed = 0 +errors = list() + +def check(filename): + global passed, failed + + print "CHECKING ", filename + + f = open(filename).read() + try: + _ = json.loads(f) + print "PASSED: ", filename + passed += 1 + return True + except ValueError as e: + failed += 1 + print "FAILED: ", filename + errors.append("FILE: " + filename) + errors.append(e) + return False + + return False + +if __name__ == "__main__": + for filename in sys.argv[1:]: + check(filename) + + print "Passed: " + str(passed) + " Failed: " + str(failed) + print "\n" + print "Showing errors:" + if failed > 0: + for err in errors: + print err + + sys.exit("JSON check Failed with errors") diff --git a/pokecli.py b/pokecli.py index 55348cae4c..cec7890894 100644 --- a/pokecli.py +++ b/pokecli.py @@ -467,7 +467,7 @@ def _json_loader(filename): config.release = load.get('release', {}) config.plugins = load.get('plugins', []) config.raw_tasks = load.get('tasks', []) - + config.daily_catch_limit = load.get('daily_catch_limit', 800) config.vips = load.get('vips', {}) if config.map_object_cache_time < 0.0: diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 62d3969ba7..8a3b319e5a 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -29,6 +29,7 @@ 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 pokemongo_bot.datastore import _init_database, Datastore from worker_result import WorkerResult from tree_config_builder import ConfigException, MismatchTaskApiVersion, TreeConfigBuilder from inventory import init_inventory @@ -36,7 +37,7 @@ import struct -class PokemonGoBot(object): +class PokemonGoBot(Datastore): @property def position(self): return self.api._position_lat, self.api._position_lng, 0 @@ -55,7 +56,13 @@ def player_data(self): return self._player def __init__(self, config): + + # Database connection MUST be setup before migrations will work + self.database = _init_database('/data/{}.db'.format(config.username)) + self.config = config + super(PokemonGoBot, self).__init__() + self.fort_timeouts = dict() self.pokemon_list = json.load( open(os.path.join(_base_dir, 'data', 'pokemon.json')) @@ -170,6 +177,22 @@ def _register_events(self): 'wake' ) ) + + # random pause + self.event_manager.register_event( + 'next_random_pause', + parameters=( + 'time', + 'duration' + ) + ) + self.event_manager.register_event( + 'bot_random_pause', + parameters=( + 'time_hms', + 'resume' + ) + ) # fort stuff self.event_manager.register_event( @@ -276,6 +299,7 @@ def _register_events(self): self.event_manager.register_event( 'threw_pokeball', parameters=( + 'throw_type', 'ball_name', 'success_percentage', 'count_left' @@ -316,6 +340,7 @@ def _register_events(self): 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')) + self.event_manager.register_event('catch_limit') # level up stuff self.event_manager.register_event( @@ -547,8 +572,11 @@ def update_web_location(self, cells=[], lat=None, lng=None, alt=None): cells = self.find_close_cells(*location) 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) + try: + with open(user_data_cells, 'w') as outfile: + json.dump(cells, outfile) + except IOError as e: + self.logger.info('[x] Error while opening location file: %s' % e) user_web_location = os.path.join( _base_dir, 'web', 'location-%s.json' % self.config.username @@ -679,6 +707,9 @@ def login(self): ) time.sleep(10) + with self.database as conn: + conn.execute('''INSERT INTO login (timestamp, message) VALUES (?, ?)''', (time.time(), 'LOGIN_SUCCESS')) + self.event_manager.emit( 'login_successful', sender=self, @@ -704,10 +735,10 @@ 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 encrypt_location in config.') - self.logger.info('Platform: '+ _platform + ' Encrypt.so directory: '+ path) + self.logger.info('Platform: '+ _platform + ' ' + file_name + ' directory: '+ path) sys.exit(1) else: - self.logger.info('Found '+ file_name +'! Platform: ' + _platform + ' Encrypt.so directory: ' + path) + self.logger.info('Found '+ file_name +'! Platform: ' + _platform + ' ' + file_name + ' directory: ' + path) return full_path @@ -841,8 +872,11 @@ def current_inventory(self): 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) + try: + with open(user_web_inventory, 'w') as outfile: + json.dump(inventory_dict, outfile) + except IOError as e: + self.logger.info('[x] Error while opening location file: %s' % e) # get player items stock # ---------------------- diff --git a/pokemongo_bot/api_wrapper.py b/pokemongo_bot/api_wrapper.py index 2c51f32ff2..8730a63995 100644 --- a/pokemongo_bot/api_wrapper.py +++ b/pokemongo_bot/api_wrapper.py @@ -6,7 +6,7 @@ NoPlayerPositionSetException, EmptySubrequestChainException, UnexpectedResponseException) from pgoapi.pgoapi import PGoApi, PGoApiRequest, RpcApi -from pgoapi.protos.POGOProtos.Networking.Requests_pb2 import RequestType +from pgoapi.protos.POGOProtos.Networking.Requests.RequestType_pb2 import RequestType from human_behaviour import sleep diff --git a/pokemongo_bot/cell_workers/__init__.py b/pokemongo_bot/cell_workers/__init__.py index 217eebbebf..af7afff6c5 100644 --- a/pokemongo_bot/cell_workers/__init__.py +++ b/pokemongo_bot/cell_workers/__init__.py @@ -20,3 +20,5 @@ from sleep_schedule import SleepSchedule from update_live_stats import UpdateLiveStats from catch_pokemon import CatchPokemon +from complete_tutorial import CompleteTutorial +from random_pause import RandomPause \ No newline at end of file diff --git a/pokemongo_bot/cell_workers/catch_pokemon.py b/pokemongo_bot/cell_workers/catch_pokemon.py index b0500f6d1a..81c35c8a04 100644 --- a/pokemongo_bot/cell_workers/catch_pokemon.py +++ b/pokemongo_bot/cell_workers/catch_pokemon.py @@ -2,6 +2,8 @@ from pokemongo_bot.base_task import BaseTask from pokemongo_bot.worker_result import WorkerResult from pokemongo_bot.cell_workers import CatchVisiblePokemon, CatchLuredPokemon +from pokemongo_bot.item_list import Item +from pokemongo_bot import inventory class CatchPokemon(BaseTask): @@ -15,6 +17,11 @@ def initialize(self): self.catch_workers.append(CatchLuredPokemon(self.bot, self.config)) def work(self): + + if sum([inventory.items().get(ball.value).count for ball in + [Item.ITEM_POKE_BALL, Item.ITEM_GREAT_BALL, Item.ITEM_ULTRA_BALL]]) <= 0: + return WorkerResult.ERROR + for cw in self.catch_workers: if cw.work() == WorkerResult.RUNNING: return WorkerResult.RUNNING diff --git a/pokemongo_bot/cell_workers/complete_tutorial.py b/pokemongo_bot/cell_workers/complete_tutorial.py new file mode 100644 index 0000000000..1706bb6908 --- /dev/null +++ b/pokemongo_bot/cell_workers/complete_tutorial.py @@ -0,0 +1,136 @@ +import random + +from pokemongo_bot import logger +from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.human_behaviour import sleep + + + +class CompleteTutorial(BaseTask): + + SUPPORTED_TASK_API_VERSION = 1 + + def initialize(self): + self.api = self.bot.api + self.nickname = self.config.get('nickname','') + self.team = self.config.get('team',0) + + def should_run(self): + return True + + def work(self): + + if not self.should_run(): + return WorkerResult.SUCCESS + + if self._check_tutorial_state(): + return WorkerResult.SUCCESS + else: + return WorkerResult.ERROR + + def _check_tutorial_state(self): + self._player=self.bot.player_data + + tutorial_state = self._player.get('tutorial_state', []) + # LEGAL_SCREEN = 0 + if not 0 in tutorial_state: + sleep(2) + if self._set_tutorial_state(0): + self.logger.info('Completed legal screen') + tutorial_state = self._player.get('tutorial_state', []) + else: + return False + + # AVATAR_SELECTION = 1 + if not 1 in tutorial_state: + # TODO : choose avatar ? + sleep(3) + if self._set_tutorial_state(1): + self.logger.info('Completed avatar selection') + tutorial_state = self._player.get('tutorial_state', []) + else: + return False + + # POKEMON_CAPTURE = 3 + if not 3 in tutorial_state: + sleep(10) + if self._encounter_tutorial(): + self.logger.info('Completed first capture') + else: + self.logger.error('Error during first capture') + return False + + # NAME_SELECTION = 4 + if not 4 in tutorial_state: + if not self.nickname: + self.logger.info("No nickname defined in config") + return False + + self.logger.info(u'Trying to set {} as nickname'.format(self.nickname)) + sleep(5) + if self._set_nickname(self.nickname): + self._set_tutorial_state(4) + tutorial_state = self._player.get('tutorial_state', []) + else: + self.logger.error('Error trying to set nickname') + return False + + # FIRST_TIME_EXPERIENCE_COMPLETE = 7 + if not 7 in tutorial_state: + if self._set_tutorial_state(7): + self.logger.info('Completed first time experience') + else: + return False + + return True + + def _encounter_tutorial(self): + # You just need to call the API with the pokemon you choose + # Probably can't get MewTwo as first pokemon though + first_pokemon_id = random.choice([1, 4, 7]) + response_dict = self.api.encounter_tutorial_complete( + pokemon_id=first_pokemon_id) + try: + if response_dict['responses']['ENCOUNTER_TUTORIAL_COMPLETE']['result'] == 1: + return True + else: + self.logger.error("Error during encouter tutorial") + return False + except KeyError: + self.logger.error("KeyError during encouter tutorial") + return False + + def _set_nickname(self, nickname): + response_dict = self.api.claim_codename(codename=nickname) + try: + result = response_dict['responses']['CLAIM_CODENAME']['status'] + if result == 1: + self.logger.info(u'Name changed to {}'.format(nickname)) + return True + else: + # Would be nice to get the text directly from the proto Enum + error_codes = { + 0: 'UNSET', + 1: 'SUCCESS', + 2: 'CODENAME_NOT_AVAILABLE', + 3: 'CODENAME_NOT_VALID', + 4: 'CURRENT_OWNER', + 5: 'CODENAME_CHANGE_NOT_ALLOWED' + } + self.logger.error( + u'Error while changing nickname : {}'.format(error_codes[result])) + return False + except KeyError: + return False + + def _set_tutorial_state(self, completed): + response_dict = self.api.mark_tutorial_complete(tutorials_completed=[ + completed], send_marketing_emails=False, send_push_notifications=False) + try: + self._player = response_dict['responses'][ + 'MARK_TUTORIAL_COMPLETE']['player_data'] + return response_dict['responses']['MARK_TUTORIAL_COMPLETE']['success'] + except KeyError: + self.logger.error("KeyError while setting tutorial state") + return False diff --git a/pokemongo_bot/cell_workers/migrations/catch_log.py b/pokemongo_bot/cell_workers/migrations/catch_log.py new file mode 100644 index 0000000000..78c7b4daf8 --- /dev/null +++ b/pokemongo_bot/cell_workers/migrations/catch_log.py @@ -0,0 +1,5 @@ +from yoyo import step + +step( + "CREATE TABLE catch_log (pokemon text, cp real, iv real, encounter_id text, pokemon_id real, dated datetime DEFAULT CURRENT_TIMESTAMP)" +) diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index 7717d5db21..a598a9f69f 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -217,7 +217,7 @@ def snipe(self, pokemon): last_position = self.bot.position[0:2] self.bot.heartbeat() self._teleport_to(pokemon) - catch_worker = PokemonCatchWorker(pokemon, self.bot) + catch_worker = PokemonCatchWorker(pokemon, self.bot, self.config) api_encounter_response = catch_worker.create_encounter_api_call() time.sleep(SNIPE_SLEEP_SEC) self._teleport_back(last_position) @@ -257,9 +257,12 @@ 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) < self.min_ball and not pokemon['is_vip']: - return WorkerResult.SUCCESS + if pokeballs < 1: + if superballs < 1: + if ultraballs < 1: + return WorkerResult.SUCCESS + if not pokemon['is_vip']: + return WorkerResult.SUCCESS if self.config['snipe']: if self.snipe_high_prio_only: diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 1846cd2a66..f33167428b 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -1,5 +1,9 @@ # -*- coding: utf-8 -*- +import os +import time +import json +import logging import time from random import random, randrange from pokemongo_bot import inventory @@ -7,6 +11,9 @@ from pokemongo_bot.human_behaviour import sleep, action_delay from pokemongo_bot.inventory import Pokemon from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot.datastore import Datastore +from pokemongo_bot.base_dir import _base_dir +from datetime import datetime, timedelta CATCH_STATUS_SUCCESS = 1 CATCH_STATUS_FAILED = 2 @@ -27,31 +34,32 @@ } -class PokemonCatchWorker(BaseTask): +class PokemonCatchWorker(Datastore, BaseTask): def __init__(self, pokemon, bot, config): self.pokemon = pokemon - self.api = bot.api - self.bot = bot - self.position = bot.position - self.pokemon_list = bot.pokemon_list + super(PokemonCatchWorker, self).__init__(bot, config) + + def initialize(self): + self.api = self.bot.api + self.position = self.bot.position + self.pokemon_list = self.bot.pokemon_list self.inventory = inventory.items() self.spawn_point_guid = '' self.response_key = '' self.response_status_key = '' - + #Config - self.config = config - self.min_ultraball_to_keep = config.get('min_ultraball_to_keep', 10) - - self.catch_throw_parameters = config.get('catch_throw_parameters', {}) + self.min_ultraball_to_keep = self.config.get('min_ultraball_to_keep', 10) + + self.catch_throw_parameters = self.config.get('catch_throw_parameters', {}) self.catch_throw_parameters_spin_success_rate = self.catch_throw_parameters.get('spin_success_rate', 0.6) self.catch_throw_parameters_excellent_rate = self.catch_throw_parameters.get('excellent_rate', 0.1) self.catch_throw_parameters_great_rate = self.catch_throw_parameters.get('great_rate', 0.5) self.catch_throw_parameters_nice_rate = self.catch_throw_parameters.get('nice_rate', 0.3) self.catch_throw_parameters_normal_rate = self.catch_throw_parameters.get('normal_rate', 0.1) - self.catchsim_config = config.get('catch_simulation', {}) + self.catchsim_config = self.config.get('catch_simulation', {}) self.catchsim_catch_wait_min = self.catchsim_config.get('catch_wait_min', 2) self.catchsim_catch_wait_max = self.catchsim_config.get('catch_wait_max', 6) self.catchsim_flee_count = int(self.catchsim_config.get('flee_count', 3)) @@ -60,13 +68,17 @@ def __init__(self, pokemon, bot, config): self.catchsim_berry_wait_max = self.catchsim_config.get('berry_wait_max', 3) self.catchsim_changeball_wait_min = self.catchsim_config.get('changeball_wait_min', 2) self.catchsim_changeball_wait_max = self.catchsim_config.get('changeball_wait_max', 3) - + ############################################################################ # public methods ############################################################################ def work(self, response_dict=None): + pokeballs = self.bot.item_inventory_count(1) + superballs = self.bot.item_inventory_count(2) + ultraballs = self.bot.item_inventory_count(3) + response_dict = response_dict or self.create_encounter_api_call() # validate response @@ -93,6 +105,14 @@ def work(self, response_dict=None): if not self._should_catch_pokemon(pokemon): return WorkerResult.SUCCESS + is_vip = self._is_vip_pokemon(pokemon) + if pokeballs < 1: + if superballs < 1: + if ultraballs < 1: + return WorkerResult.SUCCESS + if not is_vip: + return WorkerResult.SUCCESS + # log encounter self.emit_event( 'pokemon_appeared', @@ -112,15 +132,33 @@ def work(self, response_dict=None): # simulate app sleep(3) + # check for VIP pokemon + if is_vip: + self.emit_event('vip_pokemon', formatted='This is a VIP pokemon. Catch!!!') + # 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) + # check catch limits before catch + with self.bot.database as conn: + c = conn.cursor() + c.execute("SELECT DISTINCT COUNT(encounter_id) FROM catch_log WHERE dated >= datetime('now','-1 day')") + + result = c.fetchone() + + while True: + max_catch = self.bot.config.daily_catch_limit + if result[0] < max_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) + break + else: + self.emit_event('catch_limit', formatted='WARNING! You have reached your daily catch limit') + break # simulate app time.sleep(5) @@ -350,15 +388,15 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): self.generate_throw_quality_parameters(throw_parameters) # try to catch pokemon! - # TODO : Log which type of throw we selected ball_count[current_ball] -= 1 self.inventory.get(current_ball).remove(1) # Take some time to throw the ball from config options action_delay(self.catchsim_catch_wait_min, self.catchsim_catch_wait_max) self.emit_event( 'threw_pokeball', - formatted='Used {ball_name}, with chance {success_percentage} ({count_left} left)', + formatted='{throw_type}! Used {ball_name}, with chance {success_percentage} ({count_left} left)', data={ + 'throw_type': throw_parameters['throw_type_label'], 'ball_name': self.inventory.get(current_ball).name, 'success_percentage': self._pct(catch_rate_by_ball[current_ball]), 'count_left': ball_count[current_ball] @@ -412,39 +450,58 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): if self._pct(catch_rate_by_ball[current_ball]) == 100: self.bot.softban = True - # pokemon caught! + # pokemon caught! elif catch_pokemon_status == CATCH_STATUS_SUCCESS: pokemon.id = response_dict['responses']['CATCH_POKEMON']['captured_pokemon_id'] self.bot.metrics.captured_pokemon(pokemon.name, pokemon.cp, pokemon.iv_display, pokemon.iv) - inventory.pokemons().add(pokemon) - self.emit_event( - 'pokemon_caught', - formatted='Captured {pokemon}! [CP {cp}] [Potential {iv}] [{iv_display}] [+{exp} exp]', - data={ - 'pokemon': pokemon.name, - 'cp': pokemon.cp, - 'iv': pokemon.iv, - 'iv_display': pokemon.iv_display, - 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']), - 'encounter_id': self.pokemon['encounter_id'], - 'latitude': self.pokemon['latitude'], - 'longitude': self.pokemon['longitude'], - 'pokemon_id': pokemon.pokemon_id - } - ) - - # We could refresh here too, but adding 3 saves a inventory request - candy = inventory.candies(True).get(pokemon.pokemon_id) - self.emit_event( - 'gained_candy', - formatted='You now have {quantity} {type} candy!', - data = { - 'quantity': candy.quantity, - 'type': candy.type, - }, - ) - self.bot.softban = False + try: + self.emit_event( + 'pokemon_caught', + formatted='Captured {pokemon}! [CP {cp}] [Potential {iv}] [{iv_display}] [+{exp} exp]', + data={ + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv, + 'iv_display': pokemon.iv_display, + 'exp': sum(response_dict['responses']['CATCH_POKEMON']['capture_award']['xp']), + 'encounter_id': self.pokemon['encounter_id'], + 'latitude': self.pokemon['latitude'], + 'longitude': self.pokemon['longitude'], + 'pokemon_id': pokemon.pokemon_id + } + + ) + with self.bot.database as conn: + conn.execute('''INSERT INTO catch_log (pokemon, cp, iv, encounter_id, pokemon_id) VALUES (?, ?, ?, ?, ?)''', (pokemon.name, pokemon.cp, pokemon.iv, str(encounter_id), pokemon.pokemon_id)) + #conn.commit() + user_data_caught = os.path.join(_base_dir, 'data', 'caught-%s.json' % self.bot.config.username) + with open(user_data_caught, 'ab') as outfile: + outfile.write(str(datetime.now())) + json.dump({ + 'pokemon': pokemon.name, + 'cp': pokemon.cp, + 'iv': pokemon.iv, + 'encounter_id': self.pokemon['encounter_id'], + 'pokemon_id': pokemon.pokemon_id + }, outfile) + outfile.write('\n') + + except IOError as e: + self.logger.info('[x] Error while opening location file: %s' % e) + + # We could refresh here too, but adding 3 saves a inventory request + candy = inventory.candies(True).get(pokemon.pokemon_id) + self.emit_event( + 'gained_candy', + formatted='You now have {quantity} {type} candy!', + data = { + 'quantity': candy.quantity, + 'type': candy.type, + }, + ) + + self.bot.softban = False break @@ -491,4 +548,4 @@ def generate_throw_quality_parameters(self, throw_parameters): # 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' + throw_parameters['throw_type_label'] = 'OK' diff --git a/pokemongo_bot/cell_workers/random_pause.py b/pokemongo_bot/cell_workers/random_pause.py new file mode 100644 index 0000000000..f01664ce47 --- /dev/null +++ b/pokemongo_bot/cell_workers/random_pause.py @@ -0,0 +1,133 @@ +from datetime import datetime as dt, timedelta +from time import sleep +from random import uniform +from pokemongo_bot.base_task import BaseTask + + +class RandomPause(BaseTask): + """Pauses the execution of the bot at a random time for a random duration + + Simulates the user doing "something random" for some time. + Example Config: + { + "type": "RandomPause", + "config": { + "min_duration": "00:00:10", + "max_duration": "00:10:00", + "min_interval": "00:05:00", + "max_interval": "01:30:00" + } + } + + Inspired from sleep_schedule. + ... In retrospect, we could have used a generic class for both. + """ + SUPPORTED_TASK_API_VERSION = 1 + + LOG_INTERVAL_SECONDS = 600 + # At least 15 second of margin, because of login + SCHEDULING_MARGIN = timedelta(seconds=15) # Skip if next pause is RESCHEDULING_MARGIN from now + + def initialize(self): + # self.bot.event_manager.register_event('sleeper_scheduled', parameters=('datetime',)) + self._process_config() + self._schedule_next_pause() + #self._calculate_current_pause() #I didn't get it... + + def work(self): + if self._should_pause_now(): + self._sleep() + self._schedule_next_pause() + self.bot.login() + + + def getSeconds(self, strTime): + ''' + Return the duration in seconds of a time string + :param strTime: string time of format %H:%M:%S + ''' + try: + x = dt.strptime(strTime, '%H:%M:%S') + seconds = int(timedelta(hours=x.hour,minutes=x.minute,seconds=x.second).total_seconds()) + except ValueError: + seconds = 0; + + if seconds < 0: + seconds = 0; + + return seconds + + def _process_config(self): + self.minDuration = self.getSeconds(self.config.get('min_duration', '00:00:10')) + self.maxDuration = self.getSeconds(self.config.get('max_duration', '00:10:00')) + self.minInterval = self.getSeconds(self.config.get('min_interval', '00:10:00')) + self.maxInterval = self.getSeconds(self.config.get('max_interval', '01:10:00')) + + if self.minDuration > self.maxDuration: + raise ValueError('random pause min_duration is bigger than random pause max_duration') #TODO there must be a more elegant way to do it... + if self.minInterval > self.maxInterval: + raise ValueError('random pause min_interval is bigger than random pause max_interval') #TODO there must be a more elegant way to do it... + + def _schedule_next_pause(self): + ''' + Schedule the time and the duration of the next pause. + ''' + self._next_pause = self._get_next_pause_schedule() + self._next_duration = self._get_next_duration() + self.emit_event( + 'next_random_pause', + formatted="Next random pause at {time}, for a duration of {duration}", + data={ + 'time': str(self._next_pause.strftime("%H:%M:%S")), + 'duration': str(timedelta(seconds=self._next_duration)) + } + ) + + def _should_pause_now(self): + if dt.now() >= (self._next_pause + timedelta(seconds=self._next_duration) + timedelta(seconds=1)): + self._schedule_next_pause() + return False + if dt.now() >= self._next_pause: + return True + + return False + + def _get_next_pause_schedule(self): + now = dt.now() + self.SCHEDULING_MARGIN + next_time = now + timedelta(seconds=int(uniform(self.minInterval, self.maxInterval))) + + # If pause time is passed add one day + if next_time <= now: + next_time += timedelta(days=1) + + return next_time + + def _get_next_duration(self): + duration = int(uniform(self.minDuration, self.maxDuration)) + return duration + + def _sleep(self): + sleep_to_go = self._next_duration + + sleep_m, sleep_s = divmod(sleep_to_go, 60) + sleep_h, sleep_m = divmod(sleep_m, 60) + sleep_hms = '%02d:%02d:%02d' % (sleep_h, sleep_m, sleep_s) + + now = dt.now() + resume = now + timedelta(seconds=sleep_to_go) + + self.emit_event( + 'bot_random_pause', + formatted="Taking a random break for {time_hms}, will resume at {resume}", + data={ + 'time_hms': sleep_hms, + 'resume': resume.strftime("%H:%M:%S") + } + ) + while sleep_to_go > 0: + if sleep_to_go < self.LOG_INTERVAL_SECONDS: + sleep(sleep_to_go) + sleep_to_go = 0 + else: + sleep(self.LOG_INTERVAL_SECONDS) + sleep_to_go -= self.LOG_INTERVAL_SECONDS diff --git a/pokemongo_bot/cell_workers/transfer_pokemon.py b/pokemongo_bot/cell_workers/transfer_pokemon.py index 23906d6d3e..c03bbcab84 100644 --- a/pokemongo_bot/cell_workers/transfer_pokemon.py +++ b/pokemongo_bot/cell_workers/transfer_pokemon.py @@ -4,8 +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, Pokemon - +from pokemongo_bot.inventory import Pokemons, Pokemon, Attack class TransferPokemon(BaseTask): SUPPORTED_TASK_API_VERSION = 1 @@ -19,10 +18,13 @@ def work(self): 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) - + #TODO continue list possible criteria + keep_best_possible_criteria = ['cp','iv', 'iv_attack', 'iv_defense', 'iv_stamina', 'moveset.attack_perfection','moveset.defense_perfection','hp','hp_max'] + keep_best_custom, keep_best_criteria, keep_amount = self._validate_keep_best_config_custom(pokemon_name, keep_best_possible_criteria) + + best_pokemon_ids = set() + order_criteria = 'none' 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] @@ -36,8 +38,14 @@ def work(self): if order_criteria == 'cp': order_criteria = 'cp and iv' else: - order_criteria = 'iv' - + order_criteria = 'iv' + elif keep_best_custom: + limit = keep_amount + best_pokemons = sorted(group, key=lambda x: keep_best_criteria, reverse=True)[:limit] + best_pokemon_ids = set(pokemon.id for pokemon in best_pokemons) + order_criteria = ' and '.join(keep_best_criteria) + + if keep_best or keep_best_custom: # remove best pokemons from all pokemons array all_pokemons = group best_pokemons = [] @@ -76,7 +84,7 @@ def _release_pokemon_get_groups(self): continue group_id = pokemon.pokemon_id - + if group_id not in pokemon_groups: pokemon_groups[group_id] = [] @@ -178,6 +186,32 @@ def _get_release_config_for(self, pokemon): release_config = {} return release_config + def _validate_keep_best_config_custom(self, pokemon_name, keep_best_possible_custom): + keep_best = False + + release_config = self._get_release_config_for(pokemon_name) + keep_best_custom = release_config.get('keep_best_custom', '') + keep_amount = release_config.get('amount', 0) + + if keep_best_custom and keep_amount: + keep_best = True + + keep_best_custom = keep_best_custom.split(',') + for _str in keep_best_custom: + if _str not in keep_best_possible_custom: + keep_best = False + break + + try: + keep_amount = int(keep_amount) + except ValueError: + keep_best = False + + if keep_amount < 0: + keep_best = False + + return keep_best, keep_best_custom, keep_amount + def _validate_keep_best_config(self, pokemon_name): keep_best = False @@ -185,7 +219,7 @@ def _validate_keep_best_config(self, pokemon_name): keep_best_cp = release_config.get('keep_best_cp', 0) keep_best_iv = release_config.get('keep_best_iv', 0) - + if keep_best_cp or keep_best_iv: keep_best = True try: @@ -197,7 +231,7 @@ def _validate_keep_best_config(self, pokemon_name): keep_best_iv = int(keep_best_iv) except ValueError: keep_best_iv = 0 - + if keep_best_cp < 0 or keep_best_iv < 0: keep_best = False diff --git a/pokemongo_bot/cell_workers/update_live_stats.py b/pokemongo_bot/cell_workers/update_live_stats.py index ecfdeea6eb..f4f10e3762 100644 --- a/pokemongo_bot/cell_workers/update_live_stats.py +++ b/pokemongo_bot/cell_workers/update_live_stats.py @@ -56,6 +56,7 @@ class UpdateLiveStats(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. + - location : The location where the player is located. """ SUPPORTED_TASK_API_VERSION = 1 @@ -244,6 +245,7 @@ def _get_stats_line(self, player_stats): 'stardust_earned': 'Earned {:,} Stardust'.format(stardust_earned), 'highest_cp_pokemon': 'Highest CP pokemon : {}'.format(highest_cp_pokemon), 'most_perfect_pokemon': 'Most perfect pokemon : {}'.format(most_perfect_pokemon), + 'location': 'Location : ({}, {})'.format(self.bot.position[0], self.bot.position[1]), } def get_stat(stat): diff --git a/pokemongo_bot/datastore.py b/pokemongo_bot/datastore.py new file mode 100644 index 0000000000..d0f4099d3c --- /dev/null +++ b/pokemongo_bot/datastore.py @@ -0,0 +1,65 @@ +""" +Use the built-in sqlite3 library as a datastore, + +To handle database migrations, use the `yoyo-migrations` package. +For further details on this, see: +https://pypi.python.org/pypi/yoyo-migrations +""" + +import inspect +import os +import sys +import warnings + +try: + from yoyo import read_migrations, get_backend +except ImportError: + warnings.warn('Please run `pip install -r requirements.txt` to ensure you have the latest required packages') + sys.exit(-1) + + +_DEFAULT = object() + +BACKEND = _DEFAULT +DATABASE = _DEFAULT + +def _init_database(connection_string=':memory:', driver='sqlite'): + global BACKEND, DATABASE + + if DATABASE is _DEFAULT: + BACKEND = get_backend('{driver}://{conn}'.format(driver=driver, conn=connection_string)) + DATABASE = BACKEND.connection + + return DATABASE + +class Datastore(object): + MIGRATIONS_PATH = _DEFAULT + + def __init__(self, *args, **kwargs): + """ + When a subclass is initiated, the migrations should automatically be run. + """ + if _DEFAULT in (BACKEND, DATABASE): + raise RuntimeError('Migration database connection not setup. Need to run `_init_database`') + + # Init parents with additional params we may have received + super(Datastore, self).__init__(*args, **kwargs) + + path = self.MIGRATIONS_PATH + + if path is _DEFAULT: + # `migrations` should be a sub directory of the calling package, unless a path is specified + filename = inspect.stack()[1][1] + path = os.path.join(os.path.dirname(filename), 'migrations') + elif not os.path.isdir(str(path)): + raise RuntimeError('The migrations directory does not exist') + + try: + migrations = read_migrations(path) + BACKEND.apply_migrations(BACKEND.to_apply(migrations)) + except (IOError, OSError): + """ + If `migrations` directory is not present, then whatever is subclassing + us will not have any DB schemas to load. + """ + pass diff --git a/pokemongo_bot/event_handlers/colored_logging_handler.py b/pokemongo_bot/event_handlers/colored_logging_handler.py index cedd0e53dc..cd90012a22 100644 --- a/pokemongo_bot/event_handlers/colored_logging_handler.py +++ b/pokemongo_bot/event_handlers/colored_logging_handler.py @@ -11,6 +11,7 @@ class ColoredLoggingHandler(EventHandler): 'api_error': 'red', 'bot_exit': 'red', 'bot_start': 'green', + 'catch_limit': 'red', 'config_error': 'red', 'egg_already_incubating': 'yellow', 'egg_hatched': 'green', @@ -32,6 +33,7 @@ class ColoredLoggingHandler(EventHandler): 'move_to_map_pokemon_fail': 'red', 'next_egg_incubates': 'yellow', 'next_sleep': 'green', + 'next_random_pause': 'green', 'no_pokeballs': 'red', 'pokemon_appeared': 'yellow', 'pokemon_capture_failed': 'red', @@ -58,6 +60,7 @@ class ColoredLoggingHandler(EventHandler): 'arrived_at_cluster': 'white', 'arrived_at_fort': 'white', 'bot_sleep': 'white', + 'bot_random_pause': 'white', 'catchable_pokemon': 'white', 'found_cluster': 'white', 'incubate_try': 'white', diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index 62a595b07d..eea820c717 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1071,8 +1071,11 @@ def refresh(self): 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) + try: + with open(user_web_inventory, 'w') as outfile: + json.dump(inventory, outfile) + except IOError as e: + errmsg = '[x] Error while opening location file: user_web_inventory' def retrieve_item_inventory_size(self): """ diff --git a/pokemongo_bot/migrations/pokemongobot.py b/pokemongo_bot/migrations/pokemongobot.py new file mode 100644 index 0000000000..2a68265241 --- /dev/null +++ b/pokemongo_bot/migrations/pokemongobot.py @@ -0,0 +1,5 @@ +from yoyo import step + +step( + "CREATE TABLE login (timestamp INTEGER, message TEXT)", +) diff --git a/requirements.txt b/requirements.txt index 4179e1b002..cab87547bc 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@12b2028cbf19342efd971020033027802b569769#egg=pgoapi geopy==1.11.0 protobuf==3.0.0b4 requests==2.10.0 @@ -23,3 +23,4 @@ timeout-decorator==0.3.2 raven==5.23.0 demjson==2.2.4 greenlet==0.4.9 +yoyo-migrations==5.0.3