From d0f9e95172933a227ba5fc471cb39922daae3ff8 Mon Sep 17 00:00:00 2001 From: HardstuckFragger Date: Wed, 4 Dec 2024 21:25:45 -0500 Subject: [PATCH] Main Menu Fix, Old GUI, Exit Fix --- src/poetry.lock | 457 +++++------ src/pyclashbot/__main__.py | 81 +- src/pyclashbot/bot/account_switching.py | 40 +- src/pyclashbot/bot/bannerbox.py | 8 +- src/pyclashbot/bot/battlepass.py | 29 +- src/pyclashbot/bot/buy_shop_offers.py | 16 +- src/pyclashbot/bot/card_mastery_state.py | 42 +- .../bot/daily_challenge_collection.py | 8 +- src/pyclashbot/bot/deck_randomization.py | 29 +- src/pyclashbot/bot/do_fight_state.py | 195 ++--- src/pyclashbot/bot/donate.py | 19 +- src/pyclashbot/bot/level_up_chest.py | 9 +- src/pyclashbot/bot/magic_items.py | 152 ---- src/pyclashbot/bot/nav.py | 638 ++++++--------- src/pyclashbot/bot/open_chests_state.py | 13 +- src/pyclashbot/bot/request_state.py | 191 ++--- src/pyclashbot/bot/season_shop_offers.py | 13 +- src/pyclashbot/bot/states.py | 474 ++++------- src/pyclashbot/bot/trophy_road_rewards.py | 225 ++---- src/pyclashbot/bot/upgrade_all_cards.py | 1 + src/pyclashbot/bot/upgrade_state.py | 179 ++--- src/pyclashbot/bot/war_state.py | 7 +- src/pyclashbot/bot/worker.py | 21 +- src/pyclashbot/detection/image_rec.py | 9 +- .../donate_button_icon/18.png | Bin 2402 -> 0 bytes .../donate_button_icon/19.png | Bin 2682 -> 0 bytes .../donate_button_icon/20.png | Bin 3340 -> 0 bytes .../donate_button_icon/21.png | Bin 3191 -> 0 bytes .../donate_button_icon/22.png | Bin 2506 -> 0 bytes .../donate_button_icon/23.png | Bin 2722 -> 0 bytes .../donate_button_icon/24.png | Bin 3173 -> 0 bytes .../donate_button_icon/25.png | Bin 3104 -> 0 bytes .../donate_button_icon/26.png | Bin 2481 -> 0 bytes .../donate_button_icon/27.png | Bin 2766 -> 0 bytes .../donate_button_icon/28.png | Bin 2665 -> 0 bytes .../donate_button_icon/29.png | Bin 2544 -> 0 bytes .../donate_button_icon/30.png | Bin 3281 -> 0 bytes .../donate_button_icon/31.png | Bin 2213 -> 0 bytes .../donate_button_icon/32.png | Bin 2949 -> 0 bytes .../donate_button_icon/33.png | Bin 3230 -> 0 bytes .../exit_battle_button/10.png | Bin 0 -> 5039 bytes .../reference_images/exit_battle_button/9.png | Bin 0 -> 1961 bytes .../reference_images/offers_for_gold/38.png | Bin 3691 -> 0 bytes .../reference_images/offers_for_gold/39.png | Bin 3746 -> 0 bytes .../reference_images/offers_for_gold/40.png | Bin 1155 -> 0 bytes .../reference_images/offers_for_gold/41.png | Bin 1131 -> 0 bytes .../reference_images/offers_for_gold/42.png | Bin 1106 -> 0 bytes .../reference_images/offers_for_gold/43.png | Bin 1034 -> 0 bytes .../reference_images/offers_for_gold/44.png | Bin 4008 -> 0 bytes .../trophy_road_reward_claim_button/1.png | Bin 2879 -> 0 bytes .../trophy_road_reward_claim_button/2.png | Bin 2132 -> 0 bytes .../trophy_road_reward_claim_button/3.png | Bin 1994 -> 0 bytes .../trophy_road_reward_claim_button/4.png | Bin 1815 -> 0 bytes .../trophy_road_reward_claim_button/5.png | Bin 2523 -> 0 bytes .../trophy_road_reward_claim_button/6.png | Bin 1827 -> 0 bytes .../trophy_road_reward_claim_button/7.png | Bin 1773 -> 0 bytes src/pyclashbot/interface/controls.py | 116 +++ src/pyclashbot/interface/joblist.py | 45 +- src/pyclashbot/interface/layout.py | 194 ++--- src/pyclashbot/interface/stats.py | 150 ++-- src/pyclashbot/memu/client.py | 30 +- src/pyclashbot/memu/configure.py | 29 +- src/pyclashbot/memu/launcher.py | 128 ++- src/pyclashbot/memu/memu_closer.py | 2 +- src/pyclashbot/utils/logger.py | 736 +++++++++++++++--- src/pyproject.toml | 24 +- 66 files changed, 2116 insertions(+), 2194 deletions(-) delete mode 100644 src/pyclashbot/bot/magic_items.py delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/18.png delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/19.png delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/20.png delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/21.png delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/22.png delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/23.png delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/24.png delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/25.png delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/26.png delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/27.png delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/28.png delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/29.png delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/30.png delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/31.png delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/32.png delete mode 100644 src/pyclashbot/detection/reference_images/donate_button_icon/33.png create mode 100644 src/pyclashbot/detection/reference_images/exit_battle_button/10.png create mode 100644 src/pyclashbot/detection/reference_images/exit_battle_button/9.png delete mode 100644 src/pyclashbot/detection/reference_images/offers_for_gold/38.png delete mode 100644 src/pyclashbot/detection/reference_images/offers_for_gold/39.png delete mode 100644 src/pyclashbot/detection/reference_images/offers_for_gold/40.png delete mode 100644 src/pyclashbot/detection/reference_images/offers_for_gold/41.png delete mode 100644 src/pyclashbot/detection/reference_images/offers_for_gold/42.png delete mode 100644 src/pyclashbot/detection/reference_images/offers_for_gold/43.png delete mode 100644 src/pyclashbot/detection/reference_images/offers_for_gold/44.png delete mode 100644 src/pyclashbot/detection/reference_images/trophy_road_reward_claim_button/1.png delete mode 100644 src/pyclashbot/detection/reference_images/trophy_road_reward_claim_button/2.png delete mode 100644 src/pyclashbot/detection/reference_images/trophy_road_reward_claim_button/3.png delete mode 100644 src/pyclashbot/detection/reference_images/trophy_road_reward_claim_button/4.png delete mode 100644 src/pyclashbot/detection/reference_images/trophy_road_reward_claim_button/5.png delete mode 100644 src/pyclashbot/detection/reference_images/trophy_road_reward_claim_button/6.png delete mode 100644 src/pyclashbot/detection/reference_images/trophy_road_reward_claim_button/7.png create mode 100644 src/pyclashbot/interface/controls.py diff --git a/src/poetry.lock b/src/poetry.lock index bb91824d3..3ddea2c74 100644 --- a/src/poetry.lock +++ b/src/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "anyio" @@ -501,68 +501,68 @@ test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] [[package]] name = "cx-freeze" -version = "7.2.5" +version = "7.2.0" description = "Create standalone executables from Python scripts" optional = false python-versions = ">=3.8" files = [ - {file = "cx_Freeze-7.2.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2fee88d083d25ff8e25d8c8b019471d84156579983c09386f858c5993701165"}, - {file = "cx_Freeze-7.2.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e964e72765e29def25c6ecb55d7b12c6f82a65ef3bea6cdfd29ca11e8ad7bf97"}, - {file = "cx_Freeze-7.2.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce6812cf281312294de8c6d3f6c477fb7aef3882295c89f0b1a313cbdc460092"}, - {file = "cx_Freeze-7.2.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e45fd912d979acf9764672d909a17eec19c43a96044c5b91ba17d812e2e52074"}, - {file = "cx_Freeze-7.2.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4f3327800b988bf9d54636ff47423e986b649fa6f0e26e5033dfa4d7c4a86def"}, - {file = "cx_Freeze-7.2.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:eacd1b1d8b0428d3a70428a4f5671f1aab61968bee23afc804af8c22ba21c1dd"}, - {file = "cx_Freeze-7.2.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1127748fe62a888af8689c005fc8dabad586441d51370c0c49041d66b394c507"}, - {file = "cx_Freeze-7.2.5-cp310-cp310-win32.whl", hash = "sha256:bab38d4ea9d79c1c31d8e2d93f47560dd32bf84251d907f0333178c25ae5dff3"}, - {file = "cx_Freeze-7.2.5-cp310-cp310-win_amd64.whl", hash = "sha256:2ad91b52cf41e4b097967861b0b1d6698ddd4f3933ae06707477331467510825"}, - {file = "cx_Freeze-7.2.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0f69c499b37baed9ca6fe9bb89eed73514d721ae55fe262a3b337bf9322f794c"}, - {file = "cx_Freeze-7.2.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f313b12e1daf5408986906fd5040505b1be75b86a66bea8af3e164c4d3b6a0ba"}, - {file = "cx_Freeze-7.2.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:152c13fe9896d246a68fc32987e60afef46e6c8b2386f13265a8c87d54953575"}, - {file = "cx_Freeze-7.2.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:694887e5439ecbb9d21b96ebb89ee8f4f9ad21e0fb6ee86a60d0f823b1358182"}, - {file = "cx_Freeze-7.2.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8e972d72e43142490d3389e843c24af50ddf031150fdf26be8a70e26c7317244"}, - {file = "cx_Freeze-7.2.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3290c6c155fc33c81fa557bae5d648441066fb55a9ff41558b3ea2b085ba3e5b"}, - {file = "cx_Freeze-7.2.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:431771e5a199e8cffaa7a448212a06b04316b3a268a7e6bbbc509ed62339cf4f"}, - {file = "cx_Freeze-7.2.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c9933f8346416b23a6d91ec27156aa1d67a0225e69c6602ca42b7d84f65c12c5"}, - {file = "cx_Freeze-7.2.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1843a959e76f407ca86992517236dd11d2fd3568b7ae87396102d4964dc899be"}, - {file = "cx_Freeze-7.2.5-cp311-cp311-win32.whl", hash = "sha256:4419722a93a1cf2fef45c1096ea5e1ca402bfc59c0adcb326776561881292cb1"}, - {file = "cx_Freeze-7.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:975a25e9cfda1c4c956bc20e8f712416c9b24a8dbe89f56c3f1c20c6e1c77006"}, - {file = "cx_Freeze-7.2.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fcd644ac4e8938b4984a1f9af10f29bec9ddc47390f608d1cfd608d0fa3d38"}, - {file = "cx_Freeze-7.2.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de019dd0d537eac05d6bd40452d6e5b83abe3b141d9ae2c757eb68b82760093f"}, - {file = "cx_Freeze-7.2.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fb4b5bb07773fed714e59fc994a40cdc585d9e86bf01a87053906aacbe02aea"}, - {file = "cx_Freeze-7.2.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d0b660d888b18562aa06181f957ae5fcea62767df09b8fbe4613f079343d3fd"}, - {file = "cx_Freeze-7.2.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99116e7cb03d1c648c6351e7e0b9c2ed24be614118a4c52f00f8e34afa61d634"}, - {file = "cx_Freeze-7.2.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3b731c9b6c217d1b22d24a71692f481c26ea6ce14dc6d5f7b18cbd89e0f942e"}, - {file = "cx_Freeze-7.2.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a37384ca157c7f4f527d8075853622fab0870fa3481a00394704e1599ad00486"}, - {file = "cx_Freeze-7.2.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4cad6a0cdf1e198674c3fc5f84027edd1dd3dc3a9de94a4c9573c9afb7a66e7"}, - {file = "cx_Freeze-7.2.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:389f2ff47a624016e5410bfe4906357554e6f191000e658f06e87c51d649cd3b"}, - {file = "cx_Freeze-7.2.5-cp312-cp312-win32.whl", hash = "sha256:c28960966ae87b53a07519d3c1d64735f67d5c2c866d574657f26a97b033df0d"}, - {file = "cx_Freeze-7.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:e7271051c32b0afd0f504d16eb79288a4abc6d118eb7939ef38cabab68b22b9a"}, - {file = "cx_Freeze-7.2.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c8bbe721146a611c4c37847c6ecb61da3b3404c150a7c19b16a520004090ca1d"}, - {file = "cx_Freeze-7.2.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31fc0315ccd082d9c14ffc15efd95e86d3cdfe27906c6d8777a9cca7a19823a4"}, - {file = "cx_Freeze-7.2.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4c3419d957725dddd1b9c5adbbe004f5b0bd84888dfe362997db2fa754241f20"}, - {file = "cx_Freeze-7.2.5-cp38-cp38-win32.whl", hash = "sha256:2fbf6cf8198f23da8b85d8bdc3c680402465433f402cd1bedc8a7c8681d0bda0"}, - {file = "cx_Freeze-7.2.5-cp38-cp38-win_amd64.whl", hash = "sha256:d3f9c7e81b970f8d9e0d3527232cf8e4d486fc8768f185d787f21c3045c3ba24"}, - {file = "cx_Freeze-7.2.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:001a8d61e1b93425f3fee84e2cba0ddad07897da7777e576124b674f7952396c"}, - {file = "cx_Freeze-7.2.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0eaf9aaa15f472cdb05a10d20fc4e8239c3d02348945b09ad10bec777e35db8a"}, - {file = "cx_Freeze-7.2.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:03b6cb977a17134c8470f2e626cc52f026d590e900fed75e205310d61515c00d"}, - {file = "cx_Freeze-7.2.5-cp39-cp39-win32.whl", hash = "sha256:0b9a1e349e84ef9edc035451714ccd5fabe829336026d4073c987b15ccf63246"}, - {file = "cx_Freeze-7.2.5-cp39-cp39-win_amd64.whl", hash = "sha256:bcb4077f531a3559f89394b5f1ddba4e92033bd79653bd09854b705389ebdc07"}, - {file = "cx_freeze-7.2.5.tar.gz", hash = "sha256:63f9b745b8a84a1c3ce986d089f3d750ec65b9ff385ed64308c073a821d81eef"}, + {file = "cx_Freeze-7.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f843263c0faab92f71a5650f9676806378b0d2cfe5902cb2cefb3dce20a275ac"}, + {file = "cx_Freeze-7.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37df5c5184bde8a7512e08bb0c6ee72b7d26b7a72fdb71454d63530ea440ab84"}, + {file = "cx_Freeze-7.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa95b50e3746cb534a53023e3b7393b6b39ab039885302dcaeab4245066656b5"}, + {file = "cx_Freeze-7.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fcb9fb7016b28a969952761ed2ed730a8001941e29757379cc386f080bbe055"}, + {file = "cx_Freeze-7.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1c9d67de843ba748f2e52a5cf44f9f943e15ad8ec3bb590f767613b20ac24882"}, + {file = "cx_Freeze-7.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:11b86b7e5f307e67a76a8f164b9b8b0a3745418b495e6f59abd0fdfeaab206f7"}, + {file = "cx_Freeze-7.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9fbd82cdd0739daf9009c5ba0110f758f586391a00a624db94f81f3531178ea5"}, + {file = "cx_Freeze-7.2.0-cp310-cp310-win32.whl", hash = "sha256:ecea5246805c063a1d782e446c811a3c517918770fa04678637dc0f356449489"}, + {file = "cx_Freeze-7.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ac48fe678f69b965b10c5f880609e0ec738777c109d45d77e139c76385ac4404"}, + {file = "cx_Freeze-7.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0d518c75880a517b04a82258d935b9408874d7b27f6d8d20dc4d8818395d02e"}, + {file = "cx_Freeze-7.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8437c18a129b0cf875f68697a56cd86c69698e6790a5d2c61f8b970bc512128b"}, + {file = "cx_Freeze-7.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbd24c7f0f402c62bed9cfbf6e7dd7d65ef2a011d2576404d9cf2d2c113c1e48"}, + {file = "cx_Freeze-7.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e04a06dc36bfd38088879f5faab7de15c99f3daada524b8a816addb203ec9464"}, + {file = "cx_Freeze-7.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:620635e86ef46872763be8b65b7c06f5f24943151e80013e3a37f312fe070895"}, + {file = "cx_Freeze-7.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e84fb083182bcaab226b7a75d2069cefcf3e3181142b09b514d99a5ae4860c8"}, + {file = "cx_Freeze-7.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dd175db3069991b60a792ee17a2e2e4853bfbcfc0b1be976ebee04ed1098c101"}, + {file = "cx_Freeze-7.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4030de54f227b5852155d86fc00f86a19e87d0470aa650ec4ddd06b06f270ceb"}, + {file = "cx_Freeze-7.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60eba74abdb2211b38263797342e95732ec96621a781b02e5ff909cdc9cf5be5"}, + {file = "cx_Freeze-7.2.0-cp311-cp311-win32.whl", hash = "sha256:d727df584a9b1548dbc478f036382b8cde94471e106d38fb9c45b238bf8af4d1"}, + {file = "cx_Freeze-7.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:bb6d91342f7379ba76d8a07c263d1fba380f0044b13d3e4629e50cc8dd19b681"}, + {file = "cx_Freeze-7.2.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f7cf98f63c85fc2f947a7f68548136d60dfd7927a464d1cd38239a75b730f7f7"}, + {file = "cx_Freeze-7.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7f988be0ad19be0df357102fae21553f36cc4f83b4e881888e3512d1a2bcb8ba"}, + {file = "cx_Freeze-7.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:98491338f6dcf52f8ee4d872d08aeae0c91466a47383ad19d1332a565f7c4df8"}, + {file = "cx_Freeze-7.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51a2f286ff58e04510b6af87c055b6fca36045a0fb785cd966f38aa70f86c3ce"}, + {file = "cx_Freeze-7.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e6c1ccaf9be9010a0e8a8c33edc7671dd0a6f8c49ab2bcd9f49ce0960089e0"}, + {file = "cx_Freeze-7.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64c3f734cff3620d5ecc474d59a753cae52ba96b52e0a9ade803a70699be1bc1"}, + {file = "cx_Freeze-7.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d68f2ec4c0c456c6f1ca5e623f240f27fb63ae65970a16e04689863b03a16c7e"}, + {file = "cx_Freeze-7.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:77e2362f075b242a99e041afb9a49693a60c102b9b1df67f6387c87fde02e34f"}, + {file = "cx_Freeze-7.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a51b97024401d5cfaa392caef8b150bf91e62c0fdcb6a68372959c6ff28349d3"}, + {file = "cx_Freeze-7.2.0-cp312-cp312-win32.whl", hash = "sha256:19fb97c8481266931c5fd5cc380b12fb09053258413ebaf6760d5063e5ffde41"}, + {file = "cx_Freeze-7.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:e61692176a8803e0853ca115cede397942c1cee9e20c8eb5798613d0b92dd75d"}, + {file = "cx_Freeze-7.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5ed93d21a7962cc2886fb6e9e7d53fec67d6da02ff8088c9007a03835adc0240"}, + {file = "cx_Freeze-7.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b00302fc33f279f7dbca764bdb60247d609e3b424ff31198512c6eeb8cdda17b"}, + {file = "cx_Freeze-7.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e97745e663450871736f06321fc732b440628cff397f8ddbef05ffbc355b2f5d"}, + {file = "cx_Freeze-7.2.0-cp38-cp38-win32.whl", hash = "sha256:b7a0c2cced3fa9cf3b0cb4b67064b3ed2db1edec3186d75270ee0c01ed31cf54"}, + {file = "cx_Freeze-7.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:96046d864e03d4241e869fa5e3ae186be1fbdc170d77125fe36185c7a0108f6f"}, + {file = "cx_Freeze-7.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:36285f3367f4d220632c572f37140bdacb2020bdf7b166ee707e3e2351338955"}, + {file = "cx_Freeze-7.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717915dfa25730b57166b25d392c83d41e3f655c8996d473ba0368aec2a9d262"}, + {file = "cx_Freeze-7.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:afc91a24eccfe14f1853c93e78656772710ed754f7cc5ecb9761b6250a540848"}, + {file = "cx_Freeze-7.2.0-cp39-cp39-win32.whl", hash = "sha256:166b93473e86b926db7ab26e7d83504e8330df07af44cc090ebcad40e3158417"}, + {file = "cx_Freeze-7.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4a91284a925df800e0dbb324c51ab4fd7bd0ea708f280d94a2a10cf564c28cbe"}, + {file = "cx_freeze-7.2.0.tar.gz", hash = "sha256:c57f7101b4d35132464b1ec88cb8948c3b7c5b4ece4bb354c16091589cb33583"}, ] [package.dependencies] cx-Logging = {version = ">=3.1", markers = "sys_platform == \"win32\""} dmgbuild = {version = ">=1.6.1", markers = "sys_platform == \"darwin\""} filelock = {version = ">=3.12.3", markers = "sys_platform == \"linux\""} -lief = {version = ">=0.12.0,<0.16.0", markers = "sys_platform == \"win32\""} -packaging = ">=24" +lief = {version = ">=0.12.0,<0.15.0", markers = "sys_platform == \"win32\""} patchelf = {version = ">=0.14", markers = "sys_platform == \"linux\" and (platform_machine == \"aarch64\" or platform_machine == \"armv7l\" or platform_machine == \"i686\" or platform_machine == \"ppc64le\" or platform_machine == \"s390x\" or platform_machine == \"x86_64\")"} -setuptools = ">=65.6.3,<76" +setuptools = ">=65.6.3,<71" +wheel = ">=0.42.0,<=0.43.0" [package.extras] -dev = ["bump-my-version (==0.26.1)", "cibuildwheel (==2.21.1)", "pre-commit (>=3.5.0,<=3.8.0)"] -doc = ["furo (==2024.8.6)", "myst-parser (>=3.0.1,<=4.0.0)", "sphinx (>=7.1.2,<8)", "sphinx-new-tab-link (==0.6.0)", "sphinx-tabs (==3.4.5)"] -test = ["coverage (==7.6.1)", "pluggy (==1.5.0)", "pytest (==8.3.3)", "pytest-cov (==5.0.0)", "pytest-datafiles (==3.0.0)", "pytest-mock (==3.14.0)", "pytest-timeout (==2.3.1)", "pytest-xdist[psutil] (==3.6.1)"] +dev = ["bump-my-version (==0.24.2)", "cibuildwheel (==2.19.2)", "pre-commit (==3.5.0)", "pre-commit (==3.7.1)"] +doc = ["furo (==2024.5.6)", "myst-parser (==3.0.1)", "sphinx (==7.3.7)", "sphinx-new-tab-link (==0.5.0)", "sphinx-tabs (==3.4.5)"] +test = ["coverage (==7.6.0)", "pluggy (==1.5.0)", "pytest (==8.2.2)", "pytest-cov (==5.0.0)", "pytest-datafiles (==3.0.0)", "pytest-mock (==3.14.0)", "pytest-timeout (==2.3.1)", "pytest-xdist[psutil] (==3.6.1)"] [[package]] name = "cx-logging" @@ -1248,13 +1248,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (> [[package]] name = "jupyterlab" -version = "4.2.5" +version = "4.2.4" description = "JupyterLab computational environment" optional = false python-versions = ">=3.8" files = [ - {file = "jupyterlab-4.2.5-py3-none-any.whl", hash = "sha256:73b6e0775d41a9fee7ee756c80f58a6bed4040869ccc21411dc559818874d321"}, - {file = "jupyterlab-4.2.5.tar.gz", hash = "sha256:ae7f3a1b8cb88b4f55009ce79fa7c06f99d70cd63601ee4aa91815d054f46f75"}, + {file = "jupyterlab-4.2.4-py3-none-any.whl", hash = "sha256:807a7ec73637744f879e112060d4b9d9ebe028033b7a429b2d1f4fc523d00245"}, + {file = "jupyterlab-4.2.4.tar.gz", hash = "sha256:343a979fb9582fd08c8511823e320703281cd072a0049bcdafdc7afeda7f2537"}, ] [package.dependencies] @@ -1762,13 +1762,13 @@ files = [ [[package]] name = "notebook" -version = "7.2.2" +version = "7.2.1" description = "Jupyter Notebook - A web-based notebook environment for interactive computing" optional = false python-versions = ">=3.8" files = [ - {file = "notebook-7.2.2-py3-none-any.whl", hash = "sha256:c89264081f671bc02eec0ed470a627ed791b9156cad9285226b31611d3e9fe1c"}, - {file = "notebook-7.2.2.tar.gz", hash = "sha256:2ef07d4220421623ad3fe88118d687bc0450055570cdd160814a59cf3a1c516e"}, + {file = "notebook-7.2.1-py3-none-any.whl", hash = "sha256:f45489a3995746f2195a137e0773e2130960b51c9ac3ce257dbc2705aab3a6ca"}, + {file = "notebook-7.2.1.tar.gz", hash = "sha256:4287b6da59740b32173d01d641f763d292f49c30e7a51b89c46ba8473126341e"}, ] [package.dependencies] @@ -1802,64 +1802,47 @@ test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync" [[package]] name = "numpy" -version = "2.1.2" +version = "1.26.4" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.10" +python-versions = ">=3.9" files = [ - {file = "numpy-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30d53720b726ec36a7f88dc873f0eec8447fbc93d93a8f079dfac2629598d6ee"}, - {file = "numpy-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d3ca0a72dd8846eb6f7dfe8f19088060fcb76931ed592d29128e0219652884"}, - {file = "numpy-2.1.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:fc44e3c68ff00fd991b59092a54350e6e4911152682b4782f68070985aa9e648"}, - {file = "numpy-2.1.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7c1c60328bd964b53f8b835df69ae8198659e2b9302ff9ebb7de4e5a5994db3d"}, - {file = "numpy-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cdb606a7478f9ad91c6283e238544451e3a95f30fb5467fbf715964341a8a86"}, - {file = "numpy-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d666cb72687559689e9906197e3bec7b736764df6a2e58ee265e360663e9baf7"}, - {file = "numpy-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6eef7a2dbd0abfb0d9eaf78b73017dbfd0b54051102ff4e6a7b2980d5ac1a03"}, - {file = "numpy-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:12edb90831ff481f7ef5f6bc6431a9d74dc0e5ff401559a71e5e4611d4f2d466"}, - {file = "numpy-2.1.2-cp310-cp310-win32.whl", hash = "sha256:a65acfdb9c6ebb8368490dbafe83c03c7e277b37e6857f0caeadbbc56e12f4fb"}, - {file = "numpy-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:860ec6e63e2c5c2ee5e9121808145c7bf86c96cca9ad396c0bd3e0f2798ccbe2"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b42a1a511c81cc78cbc4539675713bbcf9d9c3913386243ceff0e9429ca892fe"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:faa88bc527d0f097abdc2c663cddf37c05a1c2f113716601555249805cf573f1"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c82af4b2ddd2ee72d1fc0c6695048d457e00b3582ccde72d8a1c991b808bb20f"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:13602b3174432a35b16c4cfb5de9a12d229727c3dd47a6ce35111f2ebdf66ff4"}, - {file = "numpy-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ebec5fd716c5a5b3d8dfcc439be82a8407b7b24b230d0ad28a81b61c2f4659a"}, - {file = "numpy-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2b49c3c0804e8ecb05d59af8386ec2f74877f7ca8fd9c1e00be2672e4d399b1"}, - {file = "numpy-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2cbba4b30bf31ddbe97f1c7205ef976909a93a66bb1583e983adbd155ba72ac2"}, - {file = "numpy-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8e00ea6fc82e8a804433d3e9cedaa1051a1422cb6e443011590c14d2dea59146"}, - {file = "numpy-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5006b13a06e0b38d561fab5ccc37581f23c9511879be7693bd33c7cd15ca227c"}, - {file = "numpy-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:f1eb068ead09f4994dec71c24b2844f1e4e4e013b9629f812f292f04bd1510d9"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7bf0a4f9f15b32b5ba53147369e94296f5fffb783db5aacc1be15b4bf72f43b"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b1d0fcae4f0949f215d4632be684a539859b295e2d0cb14f78ec231915d644db"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f751ed0a2f250541e19dfca9f1eafa31a392c71c832b6bb9e113b10d050cb0f1"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bd33f82e95ba7ad632bc57837ee99dba3d7e006536200c4e9124089e1bf42426"}, - {file = "numpy-2.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b8cde4f11f0a975d1fd59373b32e2f5a562ade7cde4f85b7137f3de8fbb29a0"}, - {file = "numpy-2.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d95f286b8244b3649b477ac066c6906fbb2905f8ac19b170e2175d3d799f4df"}, - {file = "numpy-2.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ab4754d432e3ac42d33a269c8567413bdb541689b02d93788af4131018cbf366"}, - {file = "numpy-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e585c8ae871fd38ac50598f4763d73ec5497b0de9a0ab4ef5b69f01c6a046142"}, - {file = "numpy-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9c6c754df29ce6a89ed23afb25550d1c2d5fdb9901d9c67a16e0b16eaf7e2550"}, - {file = "numpy-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:456e3b11cb79ac9946c822a56346ec80275eaf2950314b249b512896c0d2505e"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a84498e0d0a1174f2b3ed769b67b656aa5460c92c9554039e11f20a05650f00d"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4d6ec0d4222e8ffdab1744da2560f07856421b367928026fb540e1945f2eeeaf"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:259ec80d54999cc34cd1eb8ded513cb053c3bf4829152a2e00de2371bd406f5e"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:675c741d4739af2dc20cd6c6a5c4b7355c728167845e3c6b0e824e4e5d36a6c3"}, - {file = "numpy-2.1.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b2d4e667895cc55e3ff2b56077e4c8a5604361fc21a042845ea3ad67465aa8"}, - {file = "numpy-2.1.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43cca367bf94a14aca50b89e9bc2061683116cfe864e56740e083392f533ce7a"}, - {file = "numpy-2.1.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:76322dcdb16fccf2ac56f99048af32259dcc488d9b7e25b51e5eca5147a3fb98"}, - {file = "numpy-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:32e16a03138cabe0cb28e1007ee82264296ac0983714094380b408097a418cfe"}, - {file = "numpy-2.1.2-cp313-cp313-win32.whl", hash = "sha256:242b39d00e4944431a3cd2db2f5377e15b5785920421993770cddb89992c3f3a"}, - {file = "numpy-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f2ded8d9b6f68cc26f8425eda5d3877b47343e68ca23d0d0846f4d312ecaa445"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ffef621c14ebb0188a8633348504a35c13680d6da93ab5cb86f4e54b7e922b5"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad369ed238b1959dfbade9018a740fb9392c5ac4f9b5173f420bd4f37ba1f7a0"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d82075752f40c0ddf57e6e02673a17f6cb0f8eb3f587f63ca1eaab5594da5b17"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:1600068c262af1ca9580a527d43dc9d959b0b1d8e56f8a05d830eea39b7c8af6"}, - {file = "numpy-2.1.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a26ae94658d3ba3781d5e103ac07a876b3e9b29db53f68ed7df432fd033358a8"}, - {file = "numpy-2.1.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13311c2db4c5f7609b462bc0f43d3c465424d25c626d95040f073e30f7570e35"}, - {file = "numpy-2.1.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:2abbf905a0b568706391ec6fa15161fad0fb5d8b68d73c461b3c1bab6064dd62"}, - {file = "numpy-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ef444c57d664d35cac4e18c298c47d7b504c66b17c2ea91312e979fcfbdfb08a"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bdd407c40483463898b84490770199d5714dcc9dd9b792f6c6caccc523c00952"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:da65fb46d4cbb75cb417cddf6ba5e7582eb7bb0b47db4b99c9fe5787ce5d91f5"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c193d0b0238638e6fc5f10f1b074a6993cb13b0b431f64079a509d63d3aa8b7"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a7d80b2e904faa63068ead63107189164ca443b42dd1930299e0d1cb041cec2e"}, - {file = "numpy-2.1.2.tar.gz", hash = "sha256:13532a088217fa624c99b843eeb54640de23b3414b14aa66d023805eb731066c"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] [[package]] @@ -2088,31 +2071,31 @@ type = ["mypy (>=1.8)"] [[package]] name = "poethepoet" -version = "0.30.0" +version = "0.25.1" description = "A task runner that works well with poetry." optional = false python-versions = ">=3.8" files = [ - {file = "poethepoet-0.30.0-py3-none-any.whl", hash = "sha256:bf875741407a98da9e96f2f2d0b2c4c34f56d89939a7f53a4b6b3a64b546ec4e"}, - {file = "poethepoet-0.30.0.tar.gz", hash = "sha256:9f7ccda2d6525616ce989ca8ef973739fd668f50bef0b9d3631421d504d9ae4a"}, + {file = "poethepoet-0.25.1-py3-none-any.whl", hash = "sha256:fee433f68424593bca6b357f0bf997d64edf42c7305c0d5d335bd570b8d2352b"}, + {file = "poethepoet-0.25.1.tar.gz", hash = "sha256:98f4446533a4b2bdb08843e211f918b1f2e7f8baf6d1803ef78f64661ed62463"}, ] [package.dependencies] pastel = ">=0.2.1,<0.3.0" -pyyaml = ">=6.0.2,<7.0.0" +tomli = ">=1.2.2" [package.extras] poetry-plugin = ["poetry (>=1.0,<2.0)"] [[package]] name = "pre-commit" -version = "4.0.1" +version = "3.8.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"}, - {file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"}, + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, ] [package.dependencies] @@ -2152,33 +2135,31 @@ wcwidth = "*" [[package]] name = "psutil" -version = "6.1.0" +version = "5.9.8" description = "Cross-platform lib for process and system monitoring in Python." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"}, - {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"}, - {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:000d1d1ebd634b4efb383f4034437384e44a6d455260aaee2eca1e9c1b55f047"}, - {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5cd2bcdc75b452ba2e10f0e8ecc0b57b827dd5d7aaffbc6821b2a9a242823a76"}, - {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:045f00a43c737f960d273a83973b2511430d61f283a44c96bf13a6e829ba8fdc"}, - {file = "psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e"}, - {file = "psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85"}, - {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, - {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, - {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, - {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b"}, - {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, - {file = "psutil-6.1.0-cp36-cp36m-win32.whl", hash = "sha256:6d3fbbc8d23fcdcb500d2c9f94e07b1342df8ed71b948a2649b5cb060a7c94ca"}, - {file = "psutil-6.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1209036fbd0421afde505a4879dee3b2fd7b1e14fee81c0069807adcbbcca747"}, - {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, - {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, - {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, + {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, + {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, + {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, + {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, + {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, + {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, + {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, + {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, + {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, + {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, ] [package.extras] -dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] -test = ["pytest", "pytest-xdist", "setuptools"] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] name = "ptyprocess" @@ -2238,71 +2219,68 @@ files = [ [[package]] name = "pygame" -version = "2.6.1" +version = "2.6.0" description = "Python Game Development" optional = false python-versions = ">=3.6" files = [ - {file = "pygame-2.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9beeb647e555afb5657111fa83acb74b99ad88761108eaea66472e8b8547b55b"}, - {file = "pygame-2.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:10e3d2a55f001f6c0a6eb44aa79ea7607091c9352b946692acedb2ac1482f1c9"}, - {file = "pygame-2.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:816e85000c5d8b02a42b9834f761a5925ef3377d2924e3a7c4c143d2990ce5b8"}, - {file = "pygame-2.6.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a78fd030d98faab4a8e27878536fdff7518d3e062a72761c552f624ebba5a5f"}, - {file = "pygame-2.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da3ad64d685f84a34ebe5daacb39fff14f1251acb34c098d760d63fee768f50c"}, - {file = "pygame-2.6.1-cp310-cp310-win32.whl", hash = "sha256:9dd5c054d4bd875a8caf978b82672f02bec332f52a833a76899220c460bb4b58"}, - {file = "pygame-2.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:00827aba089355925902d533f9c41e79a799641f03746c50a374dc5c3362e43d"}, - {file = "pygame-2.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:20349195326a5e82a16e351ed93465a7845a7e2a9af55b7bc1b2110ea3e344e1"}, - {file = "pygame-2.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3935459109da4bb0b3901da9904f0a3e52028a3332a355d298b1673a334cf21"}, - {file = "pygame-2.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c31dbdb5d0217f32764797d21c2752e258e5fb7e895326538d82b5f75a0cd856"}, - {file = "pygame-2.6.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:173badf82fa198e6888017bea40f511cb28e69ecdd5a72b214e81e4dcd66c3b1"}, - {file = "pygame-2.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce8cc108b92de9b149b344ad2e25eedbe773af0dc41dfb24d1f07f679b558c60"}, - {file = "pygame-2.6.1-cp311-cp311-win32.whl", hash = "sha256:811e7b925146d8149d79193652cbb83e0eca0aae66476b1cb310f0f4226b8b5c"}, - {file = "pygame-2.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:91476902426facd4bb0dad4dc3b2573bc82c95c71b135e0daaea072ed528d299"}, - {file = "pygame-2.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4ee7f2771f588c966fa2fa8b829be26698c9b4836f82ede5e4edc1a68594942e"}, - {file = "pygame-2.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c8040ea2ab18c6b255af706ec01355c8a6b08dc48d77fd4ee783f8fc46a843bf"}, - {file = "pygame-2.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47a6938de93fa610accd4969e638c2aebcb29b2fca518a84c3a39d91ab47116"}, - {file = "pygame-2.6.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33006f784e1c7d7e466fcb61d5489da59cc5f7eb098712f792a225df1d4e229d"}, - {file = "pygame-2.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1206125f14cae22c44565c9d333607f1d9f59487b1f1432945dfc809aeaa3e88"}, - {file = "pygame-2.6.1-cp312-cp312-win32.whl", hash = "sha256:84fc4054e25262140d09d39e094f6880d730199710829902f0d8ceae0213379e"}, - {file = "pygame-2.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9e7396be0d9633831c3f8d5d82dd63ba373ad65599628294b7a4f8a5a01a65"}, - {file = "pygame-2.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6039f3a55d800db80e8010f387557b528d34d534435e0871326804df2a62f2"}, - {file = "pygame-2.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a3a1288e2e9b1e5834e425bedd5ba01a3cd4902b5c2bff8ed4a740ccfe98171"}, - {file = "pygame-2.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27eb17e3dc9640e4b4683074f1890e2e879827447770470c2aba9f125f74510b"}, - {file = "pygame-2.6.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c1623180e70a03c4a734deb9bac50fc9c82942ae84a3a220779062128e75f3b"}, - {file = "pygame-2.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef07c0103d79492c21fced9ad68c11c32efa6801ca1920ebfd0f15fb46c78b1c"}, - {file = "pygame-2.6.1-cp313-cp313-win32.whl", hash = "sha256:3acd8c009317190c2bfd81db681ecef47d5eb108c2151d09596d9c7ea9df5c0e"}, - {file = "pygame-2.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:813af4fba5d0b2cb8e58f5d95f7910295c34067dcc290d34f1be59c48bd1ea6a"}, - {file = "pygame-2.6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:56ffca6059b165bbf64f4b4be23b8068f6a0e220780e4f96ec0bb5ac3c63ec39"}, - {file = "pygame-2.6.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bede70ec708057e305815d6546012669226d1d80566785feca9b044216062e7"}, - {file = "pygame-2.6.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f84f15d146d6aa93254008a626c56ef96fed276006202881a47b29757f0cd65a"}, - {file = "pygame-2.6.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14f9dda45469b254c0f15edaaeaa85d2cc072ff6a83584a265f5d684c7f7efd8"}, - {file = "pygame-2.6.1-cp36-cp36m-win32.whl", hash = "sha256:28b43190436037e428a5be28fc80cf6615304fd528009f2c688cc828f4ff104b"}, - {file = "pygame-2.6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:a4b8f04fceddd9a3ac30778d11f0254f59efcd1c382d5801271113cea8b4f2f3"}, - {file = "pygame-2.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a620883d589926f157b8f1d1f543183ac52e5c30507dea445e3927ae0bee1c54"}, - {file = "pygame-2.6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b46e68cd168f44d0224c670bb72186688fc692d7079715f79d04096757d703d0"}, - {file = "pygame-2.6.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0b11356ac96261162d54a2c2b41a41978f00525631b01ec9c4fe26b01c66595"}, - {file = "pygame-2.6.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:325a84d072d52e3c2921eff02f87c6a74b7e77d71db3bdf53801c6c975f1b6c4"}, - {file = "pygame-2.6.1-cp37-cp37m-win32.whl", hash = "sha256:2a615d78b2364e86f541458ff41c2a46181b9a1e9eabd97b389282fdf04efbb3"}, - {file = "pygame-2.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:94afd1177680d92f9214c54966ad3517d18210c4fbc5d84a0192d218e93647e0"}, - {file = "pygame-2.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97ac4e13847b6b293ecaffa5ffce9886c98d09c03309406931cc592f0cea6366"}, - {file = "pygame-2.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d1a7f2b66ac2e4c9583b6d4c6d6f346fb10a3392c04163f537061f86a448ed5c"}, - {file = "pygame-2.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac3f033d2be4a9e23660a96afe2986df3a6916227538a6a0061bc218c5088507"}, - {file = "pygame-2.6.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1bf7ab5311bbced70320f1a56701650b4c18231343ae5af42111eea91e0949a"}, - {file = "pygame-2.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21160d9093533eb831f1b708e630706e5ac16b30750571ec27bc3b8364814f38"}, - {file = "pygame-2.6.1-cp38-cp38-win32.whl", hash = "sha256:7bffdd3eaf394d9645331d1c3a5df9d782ebcc3c5a78f3b657c7879a828dd111"}, - {file = "pygame-2.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:818b4eaec9c4acb6ac64805d4ca8edd4062bebca77bd815c18739fe2842c97e9"}, - {file = "pygame-2.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15efaa11a80a65dd589a95bebe812fa5bfc7e14946b638a424c5bd9ac6cca1a4"}, - {file = "pygame-2.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:481cfe1bdbb7fe00acc5950c494c26f00240888619bdc396fc8c39a734797432"}, - {file = "pygame-2.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d09fd950725d187aa5207c0cb8eb9ab0d2f8ce9ab8d189c30eeb470e71b617e"}, - {file = "pygame-2.6.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:163e66de169bd5670c86e27d0b74aad0d2d745e3b63cf4e7eb5b2bff1231ca8d"}, - {file = "pygame-2.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb6e8d0547f30ddc845f4fd1e33070ef548233ad0dbf21f7ecea768883d1bbdc"}, - {file = "pygame-2.6.1-cp39-cp39-win32.whl", hash = "sha256:d29eb9a93f12aa3d997b6e3c447ac85b2a4b142ab2548441523a8fcf5e216042"}, - {file = "pygame-2.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:6582aa71a681e02e55d43150a9ab41394e6bf4d783d2962a10aea58f424be060"}, - {file = "pygame-2.6.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:4a8ea113b1bf627322a025a1a5a87e3818a7f55ab3a4077ff1ae5c8c60576614"}, - {file = "pygame-2.6.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b7f9f8e6f76de36f4725175d686601214af362a4f30614b4dae2240198e72e6f"}, - {file = "pygame-2.6.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bbb7167c92103a2091366e9af26d4914ba3776666e8677d3c93551353fffa626"}, - {file = "pygame-2.6.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17498a2b043bc0e795faedef1b081199c688890200aef34991c1941caa2d2c89"}, - {file = "pygame-2.6.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7103c60939bbc1e05cfc7ba3f1d2ad3bbf103b7828b82a7166a9ab6f51950146"}, - {file = "pygame-2.6.1.tar.gz", hash = "sha256:56fb02ead529cee00d415c3e007f75e0780c655909aaa8e8bf616ee09c9feb1f"}, + {file = "pygame-2.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5707aa9d029752495b3eddc1edff62e0e390a02f699b0f1ce77fe0b8c70ea4f"}, + {file = "pygame-2.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3ed0547368733b854c0d9981c982a3cdfabfa01b477d095c57bf47f2199da44"}, + {file = "pygame-2.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6050f3e95f1f16602153d616b52619c6a2041cee7040eb529f65689e9633fc3e"}, + {file = "pygame-2.6.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89be55b7e9e22e0eea08af9d6cfb97aed5da780f0b3a035803437d481a16d972"}, + {file = "pygame-2.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d65fb222eea1294cfc8206d9e5754d476a1673eb2783c03c4f70e0455320274"}, + {file = "pygame-2.6.0-cp310-cp310-win32.whl", hash = "sha256:71eebb9803cb350298de188fb7cdd3ebf13299f78d59a71c7e81efc649aae348"}, + {file = "pygame-2.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:1551852a2cd5b4139a752888f6cbeeb4a96fc0fe6e6f3f8b9d9784eb8fceab13"}, + {file = "pygame-2.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6e5e6c010b1bf429388acf4d41d7ab2f7ad8fbf241d0db822102d35c9a2eb84"}, + {file = "pygame-2.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99902f4a2f6a338057200d99b5120a600c27a9f629ca012a9b0087c045508d08"}, + {file = "pygame-2.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a284664978a1989c1e31a0888b2f70cfbcbafdfa3bb310e750b0d3366416225"}, + {file = "pygame-2.6.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:829623cee298b3dbaa1dd9f52c3051ae82f04cad7708c8c67cb9a1a4b8fd3c0b"}, + {file = "pygame-2.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6acf7949ed764487d51123f4f3606e8f76b0df167fef12ef73ef423c35fdea39"}, + {file = "pygame-2.6.0-cp311-cp311-win32.whl", hash = "sha256:3f809560c99bd1fb4716610eca0cd36412528f03da1a63841a347b71d0c604ee"}, + {file = "pygame-2.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6897ab87f9193510a774a3483e00debfe166f340ca159f544ef99807e2a44ec4"}, + {file = "pygame-2.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b834711ebc8b9d0c2a5f9bfae4403dd277b2c61bcb689e1aa630d01a1ebcf40a"}, + {file = "pygame-2.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b5ac288655e8a31a303cc286e79cc57979ed2ba19c3a14042d4b6391c1d3bed2"}, + {file = "pygame-2.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d666667b7826b0a7921b8ce0a282ba5281dfa106976c1a3b24e32a0af65ad3b1"}, + {file = "pygame-2.6.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd8848a37a7cee37854c7efb8d451334477c9f8ce7ac339c079e724dc1334a76"}, + {file = "pygame-2.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:315e7b3c1c573984f549ac5da9778ac4709b3b4e3a4061050d94eab63fa4fe31"}, + {file = "pygame-2.6.0-cp312-cp312-win32.whl", hash = "sha256:e44bde0840cc21a91c9d368846ac538d106cf0668be1a6030f48df139609d1e8"}, + {file = "pygame-2.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c429824b1f881a7a5ce3b5c2014d3d182aa45a22cea33c8347a3971a5446907"}, + {file = "pygame-2.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b832200bd8b6fc485e087bf3ef7ec1a21437258536413a5386088f5dcd3a9870"}, + {file = "pygame-2.6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:098029d01a46ea4e30620dfb7c28a577070b456c8fc96350dde05f85c0bf51b5"}, + {file = "pygame-2.6.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a858bbdeac5ec473ec9e726c55fb8fbdc2f4aad7c55110e899883738071c7c9b"}, + {file = "pygame-2.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f908762941fd99e1f66d1211d26383184f6045c45673443138b214bf48a89aa"}, + {file = "pygame-2.6.0-cp36-cp36m-win32.whl", hash = "sha256:4a63daee99d050f47d6ec7fa7dbd1c6597b8f082cdd58b6918d382d2bc31262d"}, + {file = "pygame-2.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:ace471b3849d68968e5427fc01166ef5afaf552a5c442fc2c28d3b7226786f55"}, + {file = "pygame-2.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fea019713d0c89dfd5909225aa933010100035d1cd30e6c936e8b6f00529fb80"}, + {file = "pygame-2.6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:249dbf2d51d9f0266009a380ccf0532e1a57614a1528bb2f89a802b01d61f93e"}, + {file = "pygame-2.6.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb51533ee3204e8160600b0de34eaad70eb913a182c94a7777b6051e8fc52f1"}, + {file = "pygame-2.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f637636a44712e94e5601ec69160a080214626471983dfb0b5b68aa0c61563d"}, + {file = "pygame-2.6.0-cp37-cp37m-win32.whl", hash = "sha256:e432156b6f346f4cc6cab03ce9657600093390f4c9b10bf458716b25beebfe33"}, + {file = "pygame-2.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a0194652db7874bdde7dfc69d659ca954544c012e04ae527151325bfb970f423"}, + {file = "pygame-2.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eae3ee62cc172e268121d5bd9dc406a67094d33517de3a91de3323d6ae23eb02"}, + {file = "pygame-2.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f6a58b0a5a8740a3c2cf6fc5366888bd4514561253437f093c12a9ab4fb3ecae"}, + {file = "pygame-2.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c71da36997dc7b9b4ee973fa3a5d4a6cfb2149161b5b1c08b712d2f13a63ccfe"}, + {file = "pygame-2.6.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b86771801a7fc10d9a62218f27f1d5c13341c3a27394aa25578443a9cd199830"}, + {file = "pygame-2.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4928f3acf5a9ce5fbab384c21f1245304535ffd5fb167ae92a6b4d3cdb55a3b6"}, + {file = "pygame-2.6.0-cp38-cp38-win32.whl", hash = "sha256:4faab2df9926c4d31215986536b112f0d76f711cf02f395805f1ff5df8fd55fc"}, + {file = "pygame-2.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:afbb8d97aed93dfb116fe105603dacb68f8dab05b978a40a9e4ab1b6c1f683fd"}, + {file = "pygame-2.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d11f3646b53819892f4a731e80b8589a9140343d0d4b86b826802191b241228c"}, + {file = "pygame-2.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5ef92ed93c354eabff4b85e457d4d6980115004ec7ff52a19fd38b929c3b80fb"}, + {file = "pygame-2.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc1795f2e36302882546faacd5a0191463c4f4ae2b90e7c334a7733aa4190d2"}, + {file = "pygame-2.6.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e92294fcc85c4955fe5bc6a0404e4cc870808005dc8f359e881544e3cc214108"}, + {file = "pygame-2.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0cb7bdf3ee0233a3ac02ef777c01dfe315e6d4670f1312c83b91c1ef124359a"}, + {file = "pygame-2.6.0-cp39-cp39-win32.whl", hash = "sha256:ac906478ae489bb837bf6d2ae1eb9261d658aa2c34fa5b283027a04149bda81a"}, + {file = "pygame-2.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:92cf12a9722f6f0bdc5520d8925a8f085cff9c054a2ea462fc409cba3781be27"}, + {file = "pygame-2.6.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:a6636f452fdaddf604a060849feb84c056930b6a3c036214f607741f16aac942"}, + {file = "pygame-2.6.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dc242dc15d067d10f25c5b12a1da48ca9436d8e2d72353eaf757e83612fba2f"}, + {file = "pygame-2.6.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f82df23598a281c8c342d3c90be213c8fe762a26c15815511f60d0aac6e03a70"}, + {file = "pygame-2.6.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ed2539bb6bd211fc570b1169dc4a64a74ec5cd95741e62a0ab46bd18fe08e0d"}, + {file = "pygame-2.6.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:904aaf29710c6b03a7e1a65b198f5467ed6525e8e60bdcc5e90ff8584c1d54ea"}, + {file = "pygame-2.6.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcd28f96f0fffd28e71a98773843074597e10d7f55a098e2e5bcb2bef1bdcbf5"}, + {file = "pygame-2.6.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4fad1ab33443ecd4f958dbbb67fc09fcdc7a37e26c34054e3296fb7e26ad641e"}, + {file = "pygame-2.6.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e909186d4d512add39b662904f0f79b73028fbfc4fbfdaf6f9412aed4e500e9c"}, + {file = "pygame-2.6.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79abcbf6d12fce51a955a0652ccd50b6d0a355baa27799535eaf21efb43433dd"}, + {file = "pygame-2.6.0.tar.gz", hash = "sha256:722d33ae676aa8533c1f955eded966411298831346b8d51a77dad22e46ba3e35"}, ] [[package]] @@ -2334,13 +2312,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymemuc" -version = "0.6.0" +version = "0.5.5" description = "A Python API for MEmu Android Emulator." optional = false -python-versions = "<4.0,>=3.9" +python-versions = ">=3.9,<3.13" files = [ - {file = "pymemuc-0.6.0-py3-none-any.whl", hash = "sha256:ed79165f7511a5cba2da1a5e354247c3ba91d53ab3f95ec0c1dae056f2a0db3d"}, - {file = "pymemuc-0.6.0.tar.gz", hash = "sha256:c1f5ab9c7834fa1390a2dc2adc56f263134b41876e8d338ed3d7f54b119b5a70"}, + {file = "pymemuc-0.5.5-py3-none-any.whl", hash = "sha256:c9bd7fdbd6788cd814a55e37ca4516679f992b0e41d205de8c6f6f7fc20656ad"}, + {file = "pymemuc-0.5.5.tar.gz", hash = "sha256:796afc7431e8a3261dd523d109952b9604542ebd9b18fb3338b3e8e32cfcccb4"}, ] [[package]] @@ -2922,29 +2900,29 @@ docs = ["furo (==2024.4.27)", "pyenchant (==3.2.2)", "sphinx (==7.1.2)", "sphinx [[package]] name = "ruff" -version = "0.7.4" +version = "0.6.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478"}, - {file = "ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63"}, - {file = "ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc"}, - {file = "ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172"}, - {file = "ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a"}, - {file = "ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd"}, - {file = "ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a"}, - {file = "ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac"}, - {file = "ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6"}, - {file = "ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f"}, - {file = "ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2"}, + {file = "ruff-0.6.1-py3-none-linux_armv6l.whl", hash = "sha256:b4bb7de6a24169dc023f992718a9417380301b0c2da0fe85919f47264fb8add9"}, + {file = "ruff-0.6.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:45efaae53b360c81043e311cdec8a7696420b3d3e8935202c2846e7a97d4edae"}, + {file = "ruff-0.6.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bc60c7d71b732c8fa73cf995efc0c836a2fd8b9810e115be8babb24ae87e0850"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c7477c3b9da822e2db0b4e0b59e61b8a23e87886e727b327e7dcaf06213c5cf"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a0af7ab3f86e3dc9f157a928e08e26c4b40707d0612b01cd577cc84b8905cc9"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392688dbb50fecf1bf7126731c90c11a9df1c3a4cdc3f481b53e851da5634fa5"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5278d3e095ccc8c30430bcc9bc550f778790acc211865520f3041910a28d0024"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe6d5f65d6f276ee7a0fc50a0cecaccb362d30ef98a110f99cac1c7872df2f18"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e0dd11e2ae553ee5c92a81731d88a9883af8db7408db47fc81887c1f8b672e"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d812615525a34ecfc07fd93f906ef5b93656be01dfae9a819e31caa6cfe758a1"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faaa4060f4064c3b7aaaa27328080c932fa142786f8142aff095b42b6a2eb631"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99d7ae0df47c62729d58765c593ea54c2546d5de213f2af2a19442d50a10cec9"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9eb18dfd7b613eec000e3738b3f0e4398bf0153cb80bfa3e351b3c1c2f6d7b15"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c62bc04c6723a81e25e71715aa59489f15034d69bf641df88cb38bdc32fd1dbb"}, + {file = "ruff-0.6.1-py3-none-win32.whl", hash = "sha256:9fb4c4e8b83f19c9477a8745e56d2eeef07a7ff50b68a6998f7d9e2e3887bdc4"}, + {file = "ruff-0.6.1-py3-none-win_amd64.whl", hash = "sha256:c2ebfc8f51ef4aca05dad4552bbcf6fe8d1f75b2f6af546cc47cc1c1ca916b5b"}, + {file = "ruff-0.6.1-py3-none-win_arm64.whl", hash = "sha256:3bc81074971b0ffad1bd0c52284b22411f02a11a012082a76ac6da153536e014"}, + {file = "ruff-0.6.1.tar.gz", hash = "sha256:af3ffd8c6563acb8848d33cd19a69b9bfe943667f0419ca083f8ebe4224a3436"}, ] [[package]] @@ -3069,6 +3047,17 @@ webencodings = ">=0.4" doc = ["sphinx", "sphinx_rtd_theme"] test = ["pytest", "ruff"] +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + [[package]] name = "tornado" version = "6.4.1" @@ -3168,13 +3157,13 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "vulture" -version = "2.13" +version = "2.11" description = "Find dead code" optional = false python-versions = ">=3.8" files = [ - {file = "vulture-2.13-py2.py3-none-any.whl", hash = "sha256:34793ba60488e7cccbecdef3a7fe151656372ef94fdac9fe004c52a4000a6d44"}, - {file = "vulture-2.13.tar.gz", hash = "sha256:78248bf58f5eaffcc2ade306141ead73f437339950f80045dce7f8b078e5a1aa"}, + {file = "vulture-2.11-py2.py3-none-any.whl", hash = "sha256:12d745f7710ffbf6aeb8279ba9068a24d4e52e8ed333b8b044035c9d6b823aba"}, + {file = "vulture-2.11.tar.gz", hash = "sha256:f0fbb60bce6511aad87ee0736c502456737490a82d919a44e6d92262cb35f1c2"}, ] [[package]] @@ -3230,7 +3219,21 @@ docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] +[[package]] +name = "wheel" +version = "0.43.0" +description = "A built-package format for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "wheel-0.43.0-py3-none-any.whl", hash = "sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81"}, + {file = "wheel-0.43.0.tar.gz", hash = "sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85"}, +] + +[package.extras] +test = ["pytest (>=6.0.0)", "setuptools (>=65)"] + [metadata] lock-version = "2.0" python-versions = ">=3.12.0,<3.13" -content-hash = "01dc09a537ab155c5470722d211945ba5436ea94eae376c7ea77fdf77f698748" +content-hash = "18aa61881be56e9a8788dc3a3660455170dda28a6198c2da89261d1c0bd4a9b3" diff --git a/src/pyclashbot/__main__.py b/src/pyclashbot/__main__.py index 0603b5400..530ca801d 100644 --- a/src/pyclashbot/__main__.py +++ b/src/pyclashbot/__main__.py @@ -13,7 +13,8 @@ from pyclashbot.interface import disable_keys, user_config_keys from pyclashbot.interface.joblist import no_jobs_popup from pyclashbot.interface.layout import create_window -from pyclashbot.memu.memu_closer import close_everything_memu +from pyclashbot.memu.launcher import reset_clashbot_emulator +from pyclashbot.memu.memu_closer import close_memuc_processes from pyclashbot.utils.caching import USER_SETTINGS_CACHE from pyclashbot.utils.cli_config import arg_parser from pyclashbot.utils.logger import Logger, initalize_pylogging @@ -22,14 +23,8 @@ initalize_pylogging() -TODO = """ --fix donate image rec (its slow and stupid) -""" - - def read_window( - window: sg.Window, - timeout: int = 10, + window: sg.Window, timeout: int = 10, ) -> tuple[str, dict[str, str | int]]: """Method for reading the attributes of the window args: @@ -74,6 +69,7 @@ def make_job_dictionary(values: dict[str, str | int]) -> dict[str, str | int]: "donate_toggle": values["donate_toggle"], "free_donate_toggle": values["free_donate_toggle"], "card_mastery_user_toggle": values["card_mastery_user_toggle"], + "memu_attach_mode_toggle": values["memu_attach_mode_toggle"], "free_offer_user_toggle": values["free_offer_user_toggle"], "gold_offer_user_toggle": values["gold_offer_user_toggle"], "trophy_road_1v1_battle_user_toggle": values["trophy_road_1v1_user_toggle"], @@ -97,16 +93,46 @@ def make_job_dictionary(values: dict[str, str | int]) -> dict[str, str | int]: "disable_win_track_toggle": values["disable_win_track_toggle"], "level_up_chest_user_toggle": values["level_up_chest_user_toggle"], "trophy_road_rewards_user_toggle": values["trophy_road_rewards_user_toggle"], - 'magic_items_user_toggle':values['magic_items_user_toggle'], - # "upgrade_all_cards_user_toggle": values["upgrade_all_cards_user_toggle"], + "upgrade_all_cards_user_toggle": values["upgrade_all_cards_user_toggle"], "season_shop_buys_user_toggle": values["season_shop_buys_user_toggle"], + # job increments + "trophy_road_reward_increment_user_input": values[ + "trophy_road_reward_increment_user_input" + ], + "season_shop_buys_increment_user_input": values[ + "season_shop_buys_increment_user_input" + ], + "card_upgrade_increment_user_input": values[ + "card_upgrade_increment_user_input" + ], + "shop_buy_increment_user_input": values["shop_buy_increment_user_input"], + "request_increment_user_input": values["request_increment_user_input"], + "donate_increment_user_input": values["donate_increment_user_input"], + "daily_reward_increment_user_input": values[ + "daily_reward_increment_user_input" + ], + "card_mastery_collect_increment_user_input": values[ + "card_mastery_collect_increment_user_input" + ], + "open_chests_increment_user_input": values["open_chests_increment_user_input"], + "deck_randomization_increment_user_input": values[ + "deck_randomization_increment_user_input" + ], + "war_attack_increment_user_input": values["war_attack_increment_user_input"], + "battlepass_collect_increment_user_input": values[ + "battlepass_collect_increment_user_input" + ], + "level_up_chest_increment_user_input": values[ + "level_up_chest_increment_user_input" + ], # account switching input info + "account_switching_increment_user_input": values[ + "account_switching_increment_user_input" + ], "account_switching_toggle": values["account_switching_toggle"], - "account_switch_count": int(values["account_switching_slider"]), - # memu settings - "memu_attach_mode_toggle": values["memu_attach_mode_toggle"], - "opengl_toggle": values["opengl_toggle"], - "directx_toggle": values["directx_toggle"], + "account_switching_slider": int(values["account_switching_slider"]), + "next_account": 0, + "random_account_switch_list": random_account_switch_list, } return jobs_dictionary @@ -212,13 +238,8 @@ def load_settings(settings: None | dict[str, str], window: sg.Window) -> None: if settings is not None: for key in user_config_keys: if key in settings: - if key in list(window.key_dict.keys()): - window[key].update(settings[key]) # type: ignore - else: - print( - f"This key {key} appears in saved settings, but not the active window." - ) - window.refresh() + window[key].update(settings[key]) # type: ignore + window.refresh() # refresh the window to update the layout def show_invalid_job_increment_input_popup(key) -> None: @@ -288,6 +309,8 @@ def start_button_event(logger: Logger, window: Window, values) -> WorkerThread | logger.log("No jobs are selected!") return None + # updaet logger's update_account_order_var + logger.update_account_order_var(job_dictionary["random_account_switch_list"]) logger.log("Start Button Event") logger.change_status(status="Starting the bot!") @@ -296,12 +319,10 @@ def start_button_event(logger: Logger, window: Window, values) -> WorkerThread | logger.log_job_dictionary(job_dictionary) for key in disable_keys: - if key in list(window.key_dict.keys()): - window[key].update(disabled=True) + window[key].update(disabled=True) # close existing memuc processes - print("Closing everything memu related...") - close_everything_memu() + close_memuc_processes() # setup the main thread and start it print("Starting main thread") @@ -363,9 +384,7 @@ def exit_button_event(thread) -> None: def handle_thread_finished( - window: sg.Window, - thread: WorkerThread | None, - logger: Logger, + window: sg.Window, thread: WorkerThread | None, logger: Logger, ): """Method for handling when the worker thread is finished""" # enable the start button and configuration after the thread is stopped @@ -437,6 +456,10 @@ def main_gui(start_on_run=False, settings: None | dict[str, str] = None) -> None if url is not None: webbrowser.open(url) + # refresh emulator button + elif event == "refresh_emualtor_key": + reset_clashbot_emulator(logger) + # on Help button event, open the help gui elif event == "discord": webbrowser.open("https://discord.gg/eXdVuHuaZv") diff --git a/src/pyclashbot/bot/account_switching.py b/src/pyclashbot/bot/account_switching.py index b45cdfab1..b7e1846be 100644 --- a/src/pyclashbot/bot/account_switching.py +++ b/src/pyclashbot/bot/account_switching.py @@ -10,45 +10,71 @@ from pyclashbot.utils.logger import Logger SSID_COORDS = [ - (48, 376), # 1st account, index 0 - (48, 472), # 2nd account, index 1 - (48, 567), # 3rd account, index 2 + (48, 305), # 1st account, index 0 + (48, 387), # 2nd account, index 1 + (48, 471), # 3rd account, index 2 + (48, 553), # 4th account, index 3 + (48, 631), # 5th account, index 4 + (48, 631), # 6th account, index 5 + (48, 631), # 7th account, index 6 + (48, 631), # 8th account, index 7 ] def switch_accounts(vm_index: int, logger: Logger, account_index_to_switch_to): + logger.add_switch_account_attempt() + # if not on clash main, return False if check_if_on_clash_main_menu(vm_index) is not True: logger.change_status("293587 Not on clash main to do account switching") return False # click options burger - logger.log("Opening clash main options menu") + logger.change_status("Opening clash main options menu") click(vm_index, 386, 66) time.sleep(3) # click switch SSID button - logger.log("Clicking switch SSID button") + logger.change_status("Clicking switch SSID button") click(vm_index, 221, 368) time.sleep(4) + # wait for switch ssid page + # wait_for_switch_ssid_page(vm_index, logger) + # time.sleep(10) + + # Perform the scrolling + if account_index_to_switch_to in [5, 6, 7]: + logger.change_status( + f"Scrolling down to reach account #{account_index_to_switch_to}", + ) + if account_index_to_switch_to == 5: # 6th account + custom_swipe(vm_index, 215, 400, 215, 350, 2, 1) + elif account_index_to_switch_to == 6: # 7th account + custom_swipe(vm_index, 215, 400, 215, 350, 4, 1) + elif account_index_to_switch_to == 7: # 8th account + custom_swipe(vm_index, 215, 400, 215, 350, 6, 1) + # click the account index in question account_coord = SSID_COORDS[account_index_to_switch_to] - logger.log(f"Clicking account index #{account_index_to_switch_to}") + logger.change_status(f"Clicking account index #{account_index_to_switch_to}") click(vm_index, account_coord[0], account_coord[1], clicks=3, interval=0.33) logger.change_status(f"Selected account #{account_index_to_switch_to}") + time.sleep(6) - #wait for main page logger.change_status("Waiting for clash main on new account...") if wait_for_clash_main_menu(vm_index, logger) is False: return False time.sleep(4) + if check_for_trophy_reward_menu(vm_index): handle_trophy_reward_menu(vm_index, logger, printmode=False) time.sleep(2) logger.change_status(f"Switched to account #{account_index_to_switch_to}") + # Reset logger's in_a_clan value + logger.update_in_a_clan_value(False) logger.increment_account_switches() return True diff --git a/src/pyclashbot/bot/bannerbox.py b/src/pyclashbot/bot/bannerbox.py index efe295140..aac952d36 100644 --- a/src/pyclashbot/bot/bannerbox.py +++ b/src/pyclashbot/bot/bannerbox.py @@ -10,7 +10,6 @@ BANNERBOX_ICON_ON_CLASH_MAIN_PAGE = (350, 195) FIRST_100_TICKETS_PURCHASE_BUTTON = (303, 576) SECOND_100_TICKETS_PURCHASE_BUTTON = (209, 466) -CLASH_MAIN_DEADSPACE_COORD = (20, 520) def check_if_bannerbox_icon_have_a_star(vm_index): @@ -26,7 +25,7 @@ def check_if_bannerbox_icon_have_a_star(vm_index): ] for i, p in enumerate(pixels): - + # print(p) if not pixel_is_equal(colors[i], p, tol=10): return False @@ -121,13 +120,12 @@ def collect_bannerbox_rewards(vm_index, logger: Logger) -> bool: return False # click deadspace - click(vm_index, CLASH_MAIN_DEADSPACE_COORD[0], CLASH_MAIN_DEADSPACE_COORD[1], clicks=5, interval=0.33) + click(vm_index, 10, 450, clicks=5, interval=0.33) # return true if everything went well return True - def check_for_collected_all_bannerbox_rewards_icon(vm_index): iar = numpy.asarray(screenshot(vm_index)) @@ -173,7 +171,7 @@ def check_if_bannerbox_icon_exists_on_clashmain(vm_index): ] for i, p in enumerate(pixels): - + # print(p) if not pixel_is_equal(p, colors[i], tol=35): return True diff --git a/src/pyclashbot/bot/battlepass.py b/src/pyclashbot/bot/battlepass.py index 02f9b77ef..6bf984316 100644 --- a/src/pyclashbot/bot/battlepass.py +++ b/src/pyclashbot/bot/battlepass.py @@ -59,7 +59,7 @@ def check_for_battlepass_reward_icon(vm_index): ] for i, p in enumerate(pixels): - + # print(p) if not pixel_is_equal(colors[i], p, tol=10): return False @@ -83,7 +83,7 @@ def check_if_on_battlepass_page(vm_index): ] for i, p in enumerate(pixels): - + # print(p) if not pixel_is_equal(colors[i], p, tol=10): return False @@ -135,8 +135,7 @@ def collect_1_battlepass_reward(vm_index, logger): start_time = time.time() while time.time() - start_time < timeout: claim_rewards_coord = find_claim_battlepass_rewards_button_with_delay( - vm_index, - delay=3, + vm_index, delay=3, ) if claim_rewards_coord is None: @@ -155,32 +154,20 @@ def collect_1_battlepass_reward(vm_index, logger): # find the claim rewards button again claim_rewards_coord = find_claim_battlepass_rewards_button_with_delay( - vm_index, - delay=3, + vm_index, delay=3, ) - if claim_rewards_coord is None: - logger.change_status( - """This part needs attention. This logic was written forever ago - and I cant tell if claim_rewards_coord is supposed to be None at - this point, because it CAN be. Should it return False here? Or - should it continue and try again? Can't tell. Returning False for - safety.""" - ) - return False - # claim the reward logger.change_status('Clicking "Claim Rewards" button') - click(vm_index, claim_rewards_coord[0], claim_rewards_coord[1],clicks = 3,interval = 0.5) + click(vm_index, claim_rewards_coord[0], claim_rewards_coord[1]) time.sleep(3) - # click deadspace until back to battlepass page + a little extra ;) + # click deadspace until back to battlepass page logger.log("Skipping thru this battlepass reward") while not check_if_on_battlepass_page(vm_index): - logger.log("Skipping thru this battlepass reward") + if random.randint(1, 5): + logger.log("Skipping thru this battlepass reward") click(vm_index, 404, 33) - click(vm_index, 404, 33,clicks = 5,interval = 0.5) - logger.log("Collected 1 battlepass reward") logger.increment_battlepass_collects() diff --git a/src/pyclashbot/bot/buy_shop_offers.py b/src/pyclashbot/bot/buy_shop_offers.py index a8b5156cd..23fe0c40b 100644 --- a/src/pyclashbot/bot/buy_shop_offers.py +++ b/src/pyclashbot/bot/buy_shop_offers.py @@ -15,7 +15,7 @@ ) from pyclashbot.memu.client import ( click, - screenshot,scroll_up_in_shop_page, + screenshot, scroll_down_slowly_in_shop_page, ) from pyclashbot.utils.logger import Logger @@ -34,6 +34,7 @@ def buy_shop_offers_state( print(f"gold_buy_toggle: {gold_buy_toggle}") print(f"free_offers_toggle: {free_offers_toggle}") + logger.add_shop_buy_attempt() # if not on clash main, return False if check_if_on_clash_main_menu(vm_index) is not True: @@ -76,11 +77,10 @@ def buy_shop_offers_main( logger.change_status("Failed to get to shop page to buy offers") return False - #scroll all the way to the top - scroll_up_in_shop_page(vm_index) - # scroll incrementally while searching for rewards, clicking and buying any rewards found + purchase_total = 0 + start_time = time.time() done_buying = False logger.change_status("Starting to buy offers") @@ -169,7 +169,7 @@ def search_for_gold_purchases(vm_index): def buy_offers_from_this_shop_page( - vm_index, logger:Logger, gold_buy_toggle, free_offers_toggle, + vm_index, logger, gold_buy_toggle, free_offers_toggle, ): coord = None @@ -216,15 +216,15 @@ def check_if_on_shop_page(vm_index): ] for i, p in enumerate(pixels): - + # print(p) if not pixel_is_equal(colors[i], p, tol=10): return False return True def shop_buy_tester(): - vm_index = 1 - logger = Logger(None, False) + vm_index = 12 + logger = Logger(None, None) gold_buy_toggle = True free_offers_toggle = True diff --git a/src/pyclashbot/bot/card_mastery_state.py b/src/pyclashbot/bot/card_mastery_state.py index 630facdff..8a7006f39 100644 --- a/src/pyclashbot/bot/card_mastery_state.py +++ b/src/pyclashbot/bot/card_mastery_state.py @@ -55,6 +55,7 @@ def collect_card_mastery_rewards(vm_index, logger: Logger) -> bool: logger.add_card_mastery_reward_collection() time.sleep(2) + logger.add_card_mastery_reward_collection_attempts() # get to clash main logger.change_status("Returning to clash main menu") click(vm_index, 243, 600) @@ -82,11 +83,6 @@ def collect_first_mastery_reward(vm_index): y_positions = [316, 403, 488] for y in y_positions: click(vm_index, 200, y) - time.sleep(1) - if check_for_inventory_full_popup(vm_index): - print('Inventory full popup detected!\nClicking it') - click(vm_index,260,420) - time.sleep(1) # click deadspace ds = (14, 278) @@ -129,40 +125,6 @@ def card_mastery_rewards_exist(vm_index): return True -def check_for_inventory_full_popup(vm_index): - iar = screenshot(vm_index) - pixels = [ - iar[410][220], - iar[420][225], - iar[416][225], - iar[418][230], - iar[420][240], - iar[430][250], - iar[435][260], - iar[427][270], - iar[429][280], - iar[435][290], - ] - # for p in pixels:print(p) - colors = [ -[255 ,187 ,105], -[255 ,187 ,105], -[255 ,187 ,105], -[244 ,233 ,220], -[60 ,52 ,43], -[255 ,175 ,78], -[255 ,175 ,78], -[255 ,255 ,255], -[241 ,165 ,74], -[255 ,175 ,78], - ] - for i, c in enumerate(colors): - if not pixel_is_equal(c, pixels[i], tol = 15): - return False - return True - if __name__ == "__main__": - collect_first_mastery_reward(1) - # print('\n\n\n') - # print(check_for_inventory_full_popup(1)) + collect_first_mastery_reward(0) diff --git a/src/pyclashbot/bot/daily_challenge_collection.py b/src/pyclashbot/bot/daily_challenge_collection.py index 23b64e2a8..f3af93ee0 100644 --- a/src/pyclashbot/bot/daily_challenge_collection.py +++ b/src/pyclashbot/bot/daily_challenge_collection.py @@ -4,11 +4,8 @@ from pyclashbot.bot.nav import check_if_on_clash_main_menu from pyclashbot.detection.image_rec import pixel_is_equal -from pyclashbot.utils.logger import Logger from pyclashbot.memu.client import click, screenshot -CLASH_MAIN_DEADSPACE_COORD = (20, 520) - def collect_daily_rewards_state(vm_index, logger, next_state): # First check if all rewards have already been collected @@ -42,7 +39,8 @@ def check_if_rewards_collected(vm_index) -> bool: # If all pixels match, the checkmark is present return True -def collect_challenge_rewards(vm_index, logger:Logger, rewards) -> bool: + +def collect_challenge_rewards(vm_index, logger, rewards) -> bool: # Ensure we are on the main menu of Clash if not check_if_on_clash_main_menu(vm_index): logger.change_status( @@ -170,7 +168,7 @@ def check_which_rewards_are_available(vm_index, logger): rewards = check_rewards_menu_pixels(vm_index) # click deadspace a bunch - click(vm_index, CLASH_MAIN_DEADSPACE_COORD[0], CLASH_MAIN_DEADSPACE_COORD[1], clicks=3, interval=0.33) + click(vm_index, 15, 450, clicks=3, interval=0.33) time.sleep(2) # if not on clash main, return False diff --git a/src/pyclashbot/bot/deck_randomization.py b/src/pyclashbot/bot/deck_randomization.py index b87925674..ba8184afd 100644 --- a/src/pyclashbot/bot/deck_randomization.py +++ b/src/pyclashbot/bot/deck_randomization.py @@ -16,6 +16,7 @@ def randomize_deck_state(vm_index: int, logger: Logger, next_state: str): # increment job count + logger.add_randomize_deck_attempt() # if not on clash main, return 'restart' if check_if_on_clash_main_menu(vm_index) is False: @@ -136,10 +137,9 @@ def check_for_randomize_deck_icon(vm_index): return True -def randomize_deck(vm_index: int, logger: Logger,randomizations = 3) -> bool: +def randomize_deck(vm_index: int, logger: Logger) -> bool: start_time = time.time() - # get to card page if get_to_card_page_from_clash_main(vm_index, logger) is False: logger.change_status("Failed to get to card page from main. Returning False") @@ -149,19 +149,18 @@ def randomize_deck(vm_index: int, logger: Logger,randomizations = 3) -> bool: logger.change_status("Randomizing deck 2...") click(vm_index, 145, 107) - for _ in range(randomizations): - # click on deck options - print("Click deck options") - click_deck_options(vm_index) - time.sleep(0.1) + # click on deck options + print("Click deck options") + click_deck_options(vm_index) + time.sleep(0.1) - # click random deck button - click(vm_index, 130, 187) - time.sleep(0.1) + # click random deck button + click(vm_index, 130, 187) + time.sleep(0.1) - # click OK - click(vm_index, 280, 390) - time.sleep(0.1) + # click OK + click(vm_index, 280, 390) + time.sleep(0.1) # increment logger's deck randomization sta logger.add_card_randomization() @@ -213,11 +212,11 @@ def check_for_filled_deck(vm_index): ] for i, p in enumerate(pixels): - + # print(p) if not pixel_is_equal(colors[i], p, tol=10): return False return True if __name__ == "__main__": - randomize_deck(1, Logger()) + randomize_deck(12, Logger()) diff --git a/src/pyclashbot/bot/do_fight_state.py b/src/pyclashbot/bot/do_fight_state.py index e5beb6502..b1d890bbb 100644 --- a/src/pyclashbot/bot/do_fight_state.py +++ b/src/pyclashbot/bot/do_fight_state.py @@ -4,7 +4,7 @@ import random import time from typing import Literal -import numpy +from xmlrpc.client import Boolean from pyclashbot.bot.card_detection import ( check_which_cards_are_available, @@ -75,29 +75,13 @@ (243, 469), (308, 470), ] -CLASH_MAIN_DEADSPACE_COORD = (20, 520) - - -ELIXIR_COORDS = [ - [613, 149], - [613, 165], - [613, 188], - [613, 212], - [613, 240], - [613, 262], - [613, 287], - [613, 314], - [613, 339], - [613, 364], -] -ELIXIR_COLOR = [240, 137, 244] def do_2v2_fight_state( vm_index, logger: Logger, next_state, - random_fight_mode: bool, + random_fight_mode: Boolean, called_from_launching=False, ): """Method to handle the entirety of the 2v2 battle state (start fight, do fight, end fight)""" @@ -145,7 +129,7 @@ def do_1v1_fight_state( print(f"Fight mode is {fight_mode_choosed}") # Wait for battle start - if wait_for_1v1_battle_start(vm_index, logger) is False: + if wait_for_1v1_battle_start(vm_index, logger) == "restart": logger.change_status( "Error waiting for 1v1 battle to start in do_1v1_fight_state()", ) @@ -155,12 +139,12 @@ def do_1v1_fight_state( logger.change_status("Starting fight loop") # Run regular fight loop if random mode not toggled - if not random_fight_mode and _1v1_fight_loop(vm_index, logger) is False: + if not random_fight_mode and _1v1_fight_loop(vm_index, logger) == "restart": logger.log("Failure in fight loop") return "restart" # Run random fight loop if random mode toggled - if random_fight_mode and _1v1_random_fight_loop(vm_index, logger) is False: + if random_fight_mode and _1v1_random_fight_loop(vm_index, logger) == "restart": logger.log("Failure in fight loop") return "restart" @@ -280,48 +264,6 @@ def get_current_fight_mode(vm_index): return diff2mode[min_diff] -def check_for_change_fight_mode_page(vm_index): - iar = numpy.array(screenshot(vm_index)) - pixels = [ - iar[132][184], - iar[135][190], - iar[140][195], - iar[142][200], - iar[144][225], - iar[146][233], - iar[148][237], - iar[177][129], - iar[193][149], - iar[188][169], - iar[184][189], - iar[180][209], - iar[187][269], - iar[197][290], - ] - # for p in pixels:print(p) - colors = [ - [255, 204, 85], - [245, 196, 83], - [246, 179, 69], - [204, 120, 44], - [247, 141, 27], - [250, 138, 29], - [253, 138, 33], - [250, 110, 0], - [255, 254, 254], - [255, 255, 255], - [255, 255, 255], - [247, 228, 213], - [193, 159, 135], - [219, 83, 0], - ] - for i, c in enumerate(colors): - if not pixel_is_equal(pixels[i], c, tol=10): - return False - - return True - - def set_fight_mode(vm_index, fight_mode): # fight_modes = ['trophy_road', 'path_of_legends', 'goblin_queen'] @@ -345,48 +287,30 @@ def set_fight_mode(vm_index, fight_mode): # click the type of fight click(vm_index, coord[0], coord[1]) - time.sleep(3) - - # if the fight mode change page is still open, the - # change was unsuccessful, and we must close the page - close_fight_mode_change_page_coord = (200, 140) - if check_for_change_fight_mode_page(vm_index): - print( - f"Failed to change to fight mode: {fight_mode}. This is likely because the fight mode is not unlocked yet." - ) - click(vm_index, *close_fight_mode_change_page_coord) - time.sleep(1) - return False + time.sleep(4) return True -def start_1v1_type_fight(vm_index: int, logger: Logger, mode: str) -> bool: +def start_1v1_type_fight(vm_index, mode): # fight_modes = ['trophy_road', 'path_of_legends', 'goblin_queen'] # if not on clash main, return False if check_if_on_clash_main_menu(vm_index) is not True: - print("Not on clash main for start_1v1_type_fight()") return False # verify we're on goblin queen mode if get_current_fight_mode(vm_index) != mode: - print( - f"Current {get_current_fight_mode(vm_index)} != {mode}, setting fight mode" - ) - if set_fight_mode(vm_index, mode) is False: - print("This mode isn't available yet. Doing a regular 1v1 instead...") - logger.increment_path_of_legends_fights() - logger.increment_trophy_road_fights() + set_fight_mode(vm_index, mode) # click start button - click(vm_index, 117, 376) + click(vm_index, 175, 403) return True -def start_fight(vm_index, logger, mode) -> bool: +def start_fight(vm_index, logger, mode): # fight_modes = ['trophy_road', 'path_of_legends', 'goblin_queen','2v2'] - def increment_fight_mode_count(logger, mode): + def do_job_incrementing(logger, mode): if mode == "trophy_road": logger.increment_trophy_road_fights() elif mode == "path_of_legends": @@ -397,16 +321,13 @@ def increment_fight_mode_count(logger, mode): logger.increment_2v2_fights() logger.change_status(f"Starting a {mode} fight") - increment_fight_mode_count(logger, mode) + do_job_incrementing(logger, mode) if mode == "2v2": - print(f"{mode} is of 2v2 type") return start_2v2_fight(vm_index, logger) + return start_1v1_type_fight(vm_index, mode) - print(f"{mode} is of 1v1 type") - return start_1v1_type_fight(vm_index, logger, mode) - -def start_2v2_fight(vm_index, logger: Logger) -> bool: +def start_2v2_fight(vm_index, logger: Logger) -> Boolean: """Method to handle starting a 2v2 fight""" logger.change_status(status="Start fight state") logger.change_status(status="Starting 2v2 mode") @@ -466,7 +387,7 @@ def check_for_locked_events_page(vm_index): ] for i, p in enumerate(pixels): - + # print(p) if not pixel_is_equal(p, colors[i], tol=10): return False return True @@ -556,12 +477,8 @@ def mag_dump(vm_index, logger): def wait_for_elixer( - vm_index, - logger, - random_elixer_wait, - WAIT_THRESHOLD=5000, - PLAY_THRESHOLD=10000, -) -> Literal["restart", "no battle"] | bool: + vm_index, logger, random_elixer_wait, WAIT_THRESHOLD=5000, PLAY_THRESHOLD=10000, +) -> Literal["restart", "no battle"] | Boolean: """Method to wait for 4 elixer during a battle""" start_time = time.time() @@ -596,6 +513,19 @@ def wait_for_elixer( return True +elixer_coords = [ + [613, 149], + [613, 165], + [613, 188], + [613, 212], + [613, 240], + [613, 262], + [613, 287], + [613, 314], + [613, 339], + [613, 364], +] +elixer_color = [240, 137, 244] def count_elixer(vm_index, elixer_count) -> bool: @@ -603,8 +533,8 @@ def count_elixer(vm_index, elixer_count) -> bool: iar = screenshot(vm_index) if pixel_is_equal( - iar[ELIXIR_COORDS[elixer_count - 1][0], ELIXIR_COORDS[elixer_count - 1][1]], - ELIXIR_COLOR, + iar[elixer_coords[elixer_count - 1][0], elixer_coords[elixer_count - 1][1]], + elixer_color, tol=65, ): return True @@ -612,10 +542,7 @@ def count_elixer(vm_index, elixer_count) -> bool: def end_fight_state( - vm_index, - logger: Logger, - next_state, - disable_win_tracker_toggle=True, + vm_index, logger: Logger, next_state, disable_win_tracker_toggle=True, ): """Method to handle the time after a fight and before the next state""" # count the crown score on this end-battle screen @@ -649,8 +576,7 @@ def end_fight_state( def check_if_previous_game_was_win( - vm_index, - logger: Logger, + vm_index, logger: Logger, ) -> bool | Literal["restart"]: """Method to handle the checking if the previous game was a win or loss""" logger.change_status(status="Checking if last game was a win/loss") @@ -661,7 +587,7 @@ def check_if_previous_game_was_win( return "restart" # get to clash main options menu - if get_to_activity_log(vm_index, logger, printmode=False) == "restart": + if get_to_activity_log(vm_index, logger) == "restart": logger.change_status( status="Error 8967203948 get_to_activity_log() in check_if_previous_game_was_win()", ) @@ -690,28 +616,13 @@ def check_pixels_for_win_in_battle_log(vm_index) -> bool: log to determing if the previous game was a win """ line1 = check_line_for_color( - vm_index, - x_1=47, - y_1=135, - x_2=109, - y_2=154, - color=(255, 51, 102), + vm_index, x_1=47, y_1=135, x_2=109, y_2=154, color=(255, 51, 102), ) line2 = check_line_for_color( - vm_index, - x_1=46, - y_1=152, - x_2=115, - y_2=137, - color=(255, 51, 102), + vm_index, x_1=46, y_1=152, x_2=115, y_2=137, color=(255, 51, 102), ) line3 = check_line_for_color( - vm_index, - x_1=47, - y_1=144, - x_2=110, - y_2=147, - color=(255, 51, 102), + vm_index, x_1=47, y_1=144, x_2=110, y_2=147, color=(255, 51, 102), ) if line1 and line2 and line3: @@ -834,7 +745,7 @@ def get_to_main_after_fight(vm_index, logger): time.sleep(1) print("Clicking on deadspace to close potential pop-up windows.") - click(vm_index, CLASH_MAIN_DEADSPACE_COORD[0], CLASH_MAIN_DEADSPACE_COORD[1]) + click(vm_index, 1, 435) return False @@ -868,7 +779,7 @@ def select_card_index(card_indices, last_three_cards): return random.choice(preferred_cards) if preferred_cards else None -def play_a_card(vm_index, logger) -> bool: +def play_a_card(vm_index, logger) -> Boolean: print("\n") # check which cards are available @@ -931,7 +842,6 @@ def play_a_card(vm_index, logger) -> bool: def _2v2_fight_loop(vm_index: int, logger: Logger): - # this needs comments create_default_bridge_iar(vm_index) last_three_cards = collections.deque(maxlen=3) ingame_time = time.time() @@ -989,7 +899,7 @@ def _2v2_fight_loop(vm_index: int, logger: Logger): return "good" -def _1v1_fight_loop(vm_index, logger: Logger) -> bool: +def _1v1_fight_loop(vm_index, logger: Logger) -> Literal["restart", "good"]: """Method for handling dynamicly timed 1v1 fight""" create_default_bridge_iar(vm_index) last_three_cards = collections.deque(maxlen=3) @@ -1025,7 +935,7 @@ def _1v1_fight_loop(vm_index, logger: Logger) -> bool: if wait_output == "restart": logger.change_status("Failure while waiting for elixer") - return False + return "restart" if wait_output == "no battle": logger.change_status("Not in a 1v1 battle anymore!") @@ -1049,7 +959,7 @@ def _1v1_fight_loop(vm_index, logger: Logger) -> bool: cards_played = logger.get_cards_played() logger.change_status(f"Played ~{cards_played - prev_cards_played} cards this fight") - return True + return "good" def _2v2_random_fight_loop(vm_index, logger: Logger): @@ -1078,29 +988,24 @@ def _2v2_random_fight_loop(vm_index, logger: Logger): return "good" -def _1v1_random_fight_loop(vm_index, logger) -> bool: +def _1v1_random_fight_loop(vm_index, logger): """Method for handling dynamicly timed 1v1 fight""" logger.change_status(status="Starting 1v1 battle with random plays") - fight_timeout = 5*60#5 minutes - start_time = time.time() - + mag_dump(vm_index, logger) + for _ in range(random.randint(1, 3)): + logger.add_card_played() # while in battle: while check_if_in_battle(vm_index): - if time.time() - start_time > fight_timeout: - logger.change_status("1v1_random_fight_loop() timed out. Breaking") - return False + time.sleep(8) mag_dump(vm_index, logger) for _ in range(random.randint(1, 3)): logger.add_card_played() - time.sleep(8) - - logger.change_status("Finished with 1v1 battle with random plays...") - return True + return "good" def fight_image_save_debug(vm_index, fights=2): @@ -1139,4 +1044,4 @@ def fight_image_save_debug(vm_index, fights=2): if __name__ == "__main__": - pass + fight_image_save_debug(12, fights=2) diff --git a/src/pyclashbot/bot/donate.py b/src/pyclashbot/bot/donate.py index 7c4515c23..6f44878f6 100644 --- a/src/pyclashbot/bot/donate.py +++ b/src/pyclashbot/bot/donate.py @@ -27,7 +27,6 @@ from pyclashbot.memu.client import click, screenshot, scroll_up from pyclashbot.utils.logger import Logger -CLASH_MAIN_DEADSPACE_COORD = (20, 520) def find_claim_button(vm_index): """Finds the location of the claim button for the free gift in the clan page. @@ -73,6 +72,7 @@ def donate_cards_state(vm_index, logger: Logger, next_state, free_donate_toggle: next_state: The next state to transition to. """ + logger.add_donate_attempt() donate_start_time = time.time() global only_free @@ -89,7 +89,7 @@ def donate_cards_state(vm_index, logger: Logger, next_state, free_donate_toggle: return "restart" # if logger says we're not in a clan, check if we are in a clan - if logger.is_in_clan() is False: + if logger.in_a_clan is False: logger.change_status("Checking if in a clan before donating") in_a_clan_return = donate_state_check_if_in_a_clan(vm_index, logger) if in_a_clan_return == "restart": @@ -101,11 +101,11 @@ def donate_cards_state(vm_index, logger: Logger, next_state, free_donate_toggle: if not in_a_clan_return: return next_state else: - print(f"Logger's in_a_clan value is: {logger.is_in_clan()} so skipping check") + print(f"Logger's in_a_clan value is: {logger.in_a_clan} so skipping check") # if in a clan, update logger's in_a_clan value logger.update_in_a_clan_value(True) - print(f"Set Logger's in_a_clan value to: {logger.is_in_clan()}!") + print(f"Set Logger's in_a_clan value to: {logger.in_a_clan}!") # run donate cards main if donate_cards_main(vm_index, logger) is False: @@ -171,7 +171,7 @@ def donate_state_check_if_in_a_clan( logger.change_status("Not in a clan, so can't request!") # click deadspace to leave - click(vm_index, CLASH_MAIN_DEADSPACE_COORD[0], CLASH_MAIN_DEADSPACE_COORD[1]) + click(vm_index, 15, 450) if wait_for_clash_main_menu(vm_index, logger) is False: logger.change_status( status="Error 87258301758939 Failure with wait_for_clash_main_menu", @@ -209,7 +209,7 @@ def donate_cards_main(vm_index, logger: Logger) -> bool: return False time.sleep(0.5) - logger.change_status("Scrolling up...") + logger.change_status("Scrolling up to search for more donate requests") scroll_up(vm_index) time.sleep(1) @@ -338,7 +338,9 @@ def find_donate_button_for_free(image, vm_index, region): free_coord = get_first_location(locations) if free_coord is None: + # print("it's not free") return None + # print("it is!") return [coord[1], coord[0]] return None @@ -390,4 +392,7 @@ def check_for_positive_donate_button_coords(vm_index, coord): if __name__ == "__main__": - donate_cards_main(1, Logger()) + while 1: + print(find_donate_buttons(12)) + + # print(find_donate_button(screenshot(12))) diff --git a/src/pyclashbot/bot/level_up_chest.py b/src/pyclashbot/bot/level_up_chest.py index d15de6076..255687de9 100644 --- a/src/pyclashbot/bot/level_up_chest.py +++ b/src/pyclashbot/bot/level_up_chest.py @@ -7,7 +7,6 @@ from pyclashbot.memu.client import click, screenshot from pyclashbot.utils.logger import Logger -CLASH_MAIN_DEADSPACE_COORD = (20, 520) def collect_level_up_chest(vm_index, logger: Logger) -> bool: logger.change_status("Checking level up chest") @@ -45,7 +44,7 @@ def collect_level_up_chest(vm_index, logger: Logger) -> bool: return False print("Clicking deadspace to skip thru rewards") - click(vm_index, CLASH_MAIN_DEADSPACE_COORD[0], CLASH_MAIN_DEADSPACE_COORD[1]) + click(vm_index, 19, 450) time.sleep(1) return True @@ -69,10 +68,10 @@ def check_for_level_up_chest(vm_index): ] # for p in pixels: - + # print(p) for i, p in enumerate(pixels): - + # print(p) if not pixel_is_equal(p, colors[i], tol=15): return True @@ -82,6 +81,8 @@ def check_for_level_up_chest(vm_index): def collect_level_up_chest_state(vm_index, logger, next_state): logger.change_status("Entered collect_level_up_chest_state()") + # increment attempts + logger.add_level_up_chest_attempt() print("Checking if on clash for this state") if not check_if_on_clash_main_menu(vm_index): diff --git a/src/pyclashbot/bot/magic_items.py b/src/pyclashbot/bot/magic_items.py deleted file mode 100644 index 613bc4fed..000000000 --- a/src/pyclashbot/bot/magic_items.py +++ /dev/null @@ -1,152 +0,0 @@ -"""module for spending the magic items currencies that the bot tends to max out on""" - -import time - - -from pyclashbot.bot.nav import ( - check_if_on_clash_main_menu, - get_to_collections_page,wait_for_clash_main_menu, - check_if_on_collection_page, -) -from pyclashbot.utils.logger import Logger -from pyclashbot.memu.client import click, screenshot -from pyclashbot.detection.image_rec import pixels_match_colors - - -def spend_magic_items_state(vm_index:int,logger:Logger,next_state:str)->str: - # if not on clash main, return False - if not check_if_on_clash_main_menu(vm_index): - return 'restart' - - if spend_magic_items(vm_index, logger) is False: - return 'restart' - - # return to clash main - print("Returning to clash main after spending magic items") - click(vm_index, 243, 600) - time.sleep(3) - - # wait for main - if wait_for_clash_main_menu(vm_index, logger, deadspace_click=False) is False: - logger.change_status("Failed to wait for clash main after upgrading cards") - return 'restart' - - return next_state - - -def spend_magic_items(vm_index: int, logger: Logger) -> bool: - logger.change_status("Spending magic item currencies!") - - # get to magic items page - logger.change_status("Getting to magic items page...") - if get_to_collections_page(vm_index) is False: - logger.change_status("Failed to get to magic items page") - - # select magic items tab - magic_items_tab_coords = (180, 110) - logger.log("Clicking magic items tab") - click(vm_index, *magic_items_tab_coords) - time.sleep(1) - - #spend currency until it doesnt work anymore - reward_index2name = {0:'common',1:'rare',2:'epic',3:'legendary',} - for reward_index in [0,1,2,3]: - logger.change_status(f'Spending magic item type: {reward_index2name[reward_index]}...') - while spend_rewards(vm_index, logger,reward_index) is True: - logger.change_status('Successfully spent magic items. Trying again...') - logger.increment_magic_item_buys() - logger.change_status(f'No more magic item type: {reward_index2name[reward_index]} to spend') - logger.change_status('Done spending magic item currencies') - - return True - - -def exit_spend_reward_popup(vm_index) -> bool: - deadspace = (395, 350) - start_time = time.time() - timeout = 30 # s - while not check_if_on_collection_page(vm_index): - if time.time() - start_time > timeout: - - return False - - click(vm_index, *deadspace) - time.sleep(1) - - return True - -def spend_rewards(vm_index, logger,reward_index) -> bool: - logger.change_status("Trying to spend the first reward...") - - reward_index2coord = {0:(100,300),1:(200,300),2:(300,300),3:(100,400),} - click(vm_index, *reward_index2coord[reward_index]) - time.sleep(1) - - # if the use button doesn't pop up, that mean's we're out of rewards to spend - if not check_for_use_button(vm_index): - logger.change_status("No more rewards to spend") - exit_spend_reward_popup(vm_index) - return False - - # click the use button - print("Clicking use currency button") - use_button_coord = (206, 438) - click(vm_index, *use_button_coord) - time.sleep(1) - - # click the first card to use it on - print("Using it on the first card") - first_card_coord = (80, 200) - click(vm_index, *first_card_coord) - time.sleep(1) - - # spend the currency - print("Clicking confirm spend {amount} currency") - spend_currency_coord = (200, 430) - click(vm_index, *spend_currency_coord) - - # click deadspace - print("Clicking deadspace until we get back to the collection page") - exit_spend_reward_popup(vm_index) - - return True - - -def check_for_use_button(vm_index) -> bool: - iar = screenshot(vm_index) - pixels = [ - iar[428][188], - iar[447][198], - iar[443][208], - iar[445][218], - iar[438][220], - iar[437][222], - iar[436][224], - iar[434][226], - iar[432][227], - iar[430][225], - iar[440][223], - iar[438][189], - iar[448][227], - ] - colors = [ - [255, 131, 156], - [204, 86, 118], - [236, 207, 215], - [75, 55, 61], - [208, 173, 182], - [255, 255, 255], - [101, 52, 62], - [255, 131, 156], - [255, 131, 156], - [255, 131, 156], - [255, 254, 255], - [255, 114, 149], - [255, 108, 147], - ] - - return pixels_match_colors(pixels, colors, tol=10) - - -if __name__ == "__main__": - spend_magic_items_state(1,Logger(),'next_state') diff --git a/src/pyclashbot/bot/nav.py b/src/pyclashbot/bot/nav.py index 3dc42b416..beb46398c 100644 --- a/src/pyclashbot/bot/nav.py +++ b/src/pyclashbot/bot/nav.py @@ -4,15 +4,10 @@ from pyclashbot.detection.image_rec import ( check_line_for_color, - pixels_match_colors, pixel_is_equal, region_is_color, ) -from pyclashbot.memu.client import ( - click, - custom_swipe, - screenshot, -) +from pyclashbot.memu.client import click, screenshot, scroll_down, scroll_up from pyclashbot.utils.logger import Logger _2V2_START_WAIT_TIMEOUT = 180 # s @@ -26,14 +21,14 @@ CHALLENGES_TAB_ICON_FROM_CLASH_MAIN = (380, 598) OK_BUTTON_COORDS_IN_TROPHY_REWARD_PAGE = (209, 599) CLAN_PAGE_FROM_MAIN_NAV_TIMEOUT = 240 # seconds -CLASH_MAIN_MENU_DEADSPACE_COORD = (32, 520) +CLASH_MAIN_MENU_DEADSPACE_COORD = (32, 450) OPEN_WAR_CHEST_BUTTON_COORD = (188, 415) OPENING_WAR_CHEST_DEADZONE_COORD = (5, 298) CLASH_MAIN_WAIT_TIMEOUT = 240 # s SHOP_PAGE_BUTTON: tuple[Literal[33], Literal[603]] = (33, 603) -def get_to_shop_page_from_clash_main(vm_index, logger) -> bool: +def get_to_shop_page_from_clash_main(vm_index, logger): click(vm_index, SHOP_PAGE_BUTTON[0], SHOP_PAGE_BUTTON[1]) if wait_for_clash_main_shop_page(vm_index, logger) == "restart": logger.change_status( @@ -43,7 +38,7 @@ def get_to_shop_page_from_clash_main(vm_index, logger) -> bool: return True -def wait_for_2v2_battle_start(vm_index, logger: Logger) -> bool: +def wait_for_2v2_battle_start(vm_index, logger: Logger) -> Literal["restart", "good"]: """Waits for the 2v2 battle to start. Args: @@ -76,10 +71,8 @@ def wait_for_2v2_battle_start(vm_index, logger: Logger) -> bool: def wait_for_1v1_battle_start( - vm_index, - logger: Logger, - printmode=False, -) -> bool: + vm_index, logger: Logger, printmode=False, +) -> Literal["restart", "good"]: """Waits for the 1v1 battle to start. Args: @@ -90,7 +83,8 @@ def wait_for_1v1_battle_start( Returns: ------- - bool - success status + Literal["restart", "good"]: "restart" if the function + needs to be restarted, "good" otherwise. """ start_time: float = time.time() @@ -104,7 +98,7 @@ def wait_for_1v1_battle_start( logger.change_status( status="Error 8734572456 Waiting too long for 1v1 battle to start", ) - return False + return "restart" print("Waiting for 1v1 start") click(vm_index=vm_index, x_coord=200, y_coord=200) @@ -112,10 +106,10 @@ def wait_for_1v1_battle_start( logger.change_status(status="Done waiting for 1v1 battle to start") else: logger.log(message="Done waiting for 1v1 battle to star") - return True + return "good" -def check_for_in_battle_with_delay(vm_index) -> bool: +def check_for_in_battle_with_delay(vm_index): """Checks if the virtual machine is in a 2v2 battle with a delay. Args: @@ -135,7 +129,7 @@ def check_for_in_battle_with_delay(vm_index) -> bool: return False -def check_if_in_battle(vm_index) -> Literal["2v2"] | Literal["1v1"] | Literal["None"]: +def check_if_in_battle(vm_index) -> bool: """Checks if the virtual machine is in a 1v1 or 2v2 battle. Args: @@ -173,7 +167,9 @@ def check_if_in_battle(vm_index) -> Literal["2v2"] | Literal["1v1"] | Literal["N combat_2v2_color = [169, 29, 172] if pixel_is_equal(combat_2v2_pixel, combat_2v2_color, tol=50): + # print("It's a 2v2 fight") return "2v2" + # print("It's a 1v1 fight") return "1v1" return "None" @@ -199,12 +195,7 @@ def check_if_in_battle_at_start(vm_index, logger): if battle_status == "1v1": logger.log("Detected in battle status: 1v1. Engaging in battle.") fight_result = do_1v1_fight_state( - vm_index, - logger, - "next_state", - False, - "none", - True, + vm_index, logger, "next_state", False, "none", True, ) elif battle_status == "2v2": logger.log("Detected in battle status: 2v2. Engaging in battle.") @@ -217,7 +208,9 @@ def check_if_in_battle_at_start(vm_index, logger): logger.log("Failed to return to Clash Main Menu after fight.") return "restart" logger.log("Successfully returned to Clash Main Menu after fight.") - return "good" # Indicate successful handling after the end of the battle + return ( + "good" # Indicate successful handling after the end of the battle + ) return "no" # Indicate no battle detected or no end-of-battle screen # Attempt to return to the main menu after the battle, if a fight was detected @@ -296,9 +289,7 @@ def check_end_of_battle_screen(vm_index): def get_to_clash_main_from_clan_page( - vm_index, - logger: Logger, - printmode=False, + vm_index, logger: Logger, printmode=False, ) -> Literal["restart", "good"]: """Navigates to the clash main page from the clan page. @@ -325,9 +316,7 @@ def get_to_clash_main_from_clan_page( else: logger.log(message="Clicking clash main icon") click( - vm_index, - CLASH_MAIN_COORD_FROM_CLAN_PAGE[0], - CLASH_MAIN_COORD_FROM_CLAN_PAGE[1], + vm_index, CLASH_MAIN_COORD_FROM_CLAN_PAGE[0], CLASH_MAIN_COORD_FROM_CLAN_PAGE[1], ) # wait for clash main menu @@ -366,10 +355,19 @@ def open_war_chest_obstruction(vm_index, logger): def check_for_war_chest_obstruction(vm_index): - # dont use check_line_for_color in the future. its slow + """Checks if there is a war chest obstruction on the way to getting to the clan page. + + Args: + ---- + vm_index (int): The index of the virtual machine. + + Returns: + ------- + bool: True if there is a war chest obstruction, False otherwise. + + """ if not check_line_for_color(vm_index, 213, 409, 218, 423, (252, 195, 63)): return False - if not check_line_for_color(vm_index, 156, 416, 164, 414, (255, 255, 255)): return False @@ -388,7 +386,8 @@ def collect_boot_reward(vm_index): click(vm_index, 5, 200, clicks=20, interval=0.5) -def check_for_boot_reward(iar): +def check_for_boot_reward(vm_index): + iar = screenshot(vm_index) pixels = [ iar[350][150], iar[377][174], @@ -407,6 +406,7 @@ def check_for_boot_reward(iar): [62, 199, 255], [43, 190, 255], ] + # for p in pixels:print(p) for i, p in enumerate(pixels): if not pixel_is_equal(p, colors[i], tol=25): @@ -416,74 +416,79 @@ def check_for_boot_reward(iar): def get_to_clan_tab_from_clash_main( - vm_index: int, - logger: Logger, -): + vm_index: int, logger: Logger, +) -> Literal["restart", "good"]: + """Navigates from the Clash Main page to the Clan Tab page. - # just try it raw real quick in case it works first try - click( - vm_index, - CLAN_TAB_BUTTON_COORDS_FROM_MAIN[0], - CLAN_TAB_BUTTON_COORDS_FROM_MAIN[1], - ) - time.sleep(2) - if check_if_on_clan_chat_page(screenshot(vm_index)): - return True + Args: + ---- + vm_index (int): The index of the virtual machine. + logger (Logger): The logger object. + + Returns: + ------- + Literal["restart", "good"]: "restart" if there is an error, "good" otherwise. + """ start_time = time.time() - while time.time() - start_time < CLAN_PAGE_FROM_MAIN_NAV_TIMEOUT: - iar = screenshot(vm_index) + while 1: + # timeout check + time_taken = time.time() - start_time + if time_taken > CLAN_PAGE_FROM_MAIN_NAV_TIMEOUT: + logger.change_status( + "Error 89572985 took too long to get to clan tab from clash main", + ) + return "restart" # if boot exists, collect boot - if check_for_boot_reward(iar): + if check_for_boot_reward(vm_index): collect_boot_reward(vm_index) logger.add_war_chest_collect() print(f"Incremented war chest collects to {logger.war_chest_collects}") # check for a war chest obstructing the nav - elif check_for_war_chest_obstruction(vm_index): + if check_for_war_chest_obstruction(vm_index): open_war_chest_obstruction(vm_index, logger) logger.add_war_chest_collect() print(f"Incremented war chest collects to {logger.war_chest_collects}") # if on the clan tab chat page, return - elif check_if_on_clan_chat_page(iar): - return True + if check_if_on_clan_chat_page(vm_index): + break # if on clash main, click the clan tab button - elif check_if_on_clash_main_menu(vm_index): - click( - vm_index, - CLAN_TAB_BUTTON_COORDS_FROM_MAIN[0], - CLAN_TAB_BUTTON_COORDS_FROM_MAIN[1], - ) + handle_clash_main_page_for_clan_page_navigation(vm_index) # if on final results page, click OK - elif check_for_final_results_page(vm_index): - logger.log("On final_results_page so clicking OK button") - click(vm_index, 211, 524) + handle_final_results_page(vm_index, logger) # handle daily defenses rank page handle_war_popup_pages(vm_index, logger) - # scroll_up(vm_index) - # scroll_down(vm_index) - custom_swipe(vm_index, 206, 313, 204, 417) - custom_swipe(vm_index, 204, 417, 206, 313) - click( - vm_index, - CLAN_TAB_BUTTON_COORDS_FROM_MAIN[0], - CLAN_TAB_BUTTON_COORDS_FROM_MAIN[1], - ) - time.sleep(2) + if random.randint(0, 1) == 1: + if random.randint(1, 3) == 1: + scroll_up(vm_index) + scroll_down(vm_index) + time.sleep(2) + continue + + elif random.randint(1, 3) == 1: + click( + vm_index, + CLAN_TAB_BUTTON_COORDS_FROM_MAIN[0], + CLAN_TAB_BUTTON_COORDS_FROM_MAIN[1], + ) + time.sleep(2) + continue + time.sleep(1) # if here, then done logger.log("Made it to the clan page from clash main") - return True + return "good" def handle_war_popup_pages(vm_index, logger): - timeout = 2 + timeout = 4 start_time = time.time() while time.time() - start_time < timeout: if check_for_battle_day_results_page(vm_index): @@ -500,6 +505,7 @@ def handle_war_popup_pages(vm_index, logger): ): print("Found daily_defenses page") click(vm_index, 150, 260) + time.sleep(2) logger.change_status("Handled daily defenses rank page") return True @@ -508,6 +514,7 @@ def handle_war_popup_pages(vm_index, logger): open_war_chest_obstruction(vm_index, logger) logger.add_war_chest_collect() print(f"Incremented war chest collects to {logger.war_chest_collects}") + time.sleep(1) return True return False @@ -532,6 +539,7 @@ def check_for_battle_day_results_page(vm_index): ] for i, p in enumerate(pixels): + # print(p) if not pixel_is_equal(p, colors[i], tol=25): return False return True @@ -559,6 +567,7 @@ def check_for_daily_defenses_rank_page_3(vm_index): ] for i, p in enumerate(pixels): + # print(p) if not pixel_is_equal(p, colors[i], tol=15): return False return True @@ -580,6 +589,7 @@ def check_for_daily_defenses_rank_page_4(vm_index): ] for i, p in enumerate(pixels): + # print(p) if not pixel_is_equal(p, colors[i], tol=15): return False return True @@ -607,6 +617,7 @@ def check_for_daily_defenses_rank_page_2(vm_index): ] for i, p in enumerate(pixels): + # print(p) if not pixel_is_equal(p, colors[i], tol=25): return False return True @@ -630,11 +641,51 @@ def check_for_daily_defenses_rank_page(vm_index): ] for i, p in enumerate(pixels): + # print(p) if not pixel_is_equal(p, colors[i], tol=15): return False return True +def handle_clash_main_page_for_clan_page_navigation(vm_index) -> None: + """Handles navigation from the Clash Main page to the Clan page. + + Args: + ---- + vm_index (int): The index of the virtual machine. + logger (Logger): The logger object. + + Returns: + ------- + None + + """ + if check_if_on_clash_main_menu(vm_index): + click( + vm_index, + CLAN_TAB_BUTTON_COORDS_FROM_MAIN[0], + CLAN_TAB_BUTTON_COORDS_FROM_MAIN[1], + ) + + +def handle_final_results_page(vm_index, logger) -> None: + """Handles the final results page. + + Args: + ---- + vm_index (int): The index of the virtual machine. + logger (Logger): The logger object. + + Returns: + ------- + None + + """ + if check_for_final_results_page(vm_index): + click(vm_index, 211, 524) + logger.log("On final_results_page so clicking OK button") + + def check_for_final_results_page(vm_index) -> bool: """Checks if the final results page is displayed on the screen. @@ -660,7 +711,7 @@ def check_for_final_results_page(vm_index) -> bool: return True -def check_if_on_clan_chat_page(iar) -> bool: +def check_if_on_clan_chat_page(vm_index) -> bool: """Checks if the bot is currently on the clan chat page by comparing specific pixel colors. Args: @@ -672,6 +723,7 @@ def check_if_on_clan_chat_page(iar) -> bool: bool: True if the bot is on the clan chat page, False otherwise. """ + iar = screenshot(vm_index) # Define the pixel positions and their expected colors pixels = [ (iar[15][9], (141, 84, 69)), # X: 9 Y: 15 @@ -705,39 +757,22 @@ def check_if_on_profile_page(vm_index) -> bool: """ if not check_line_for_color( - vm_index, - x_1=329, - y_1=188, - x_2=339, - y_2=195, - color=(4, 244, 88), + vm_index, x_1=329, y_1=188, x_2=339, y_2=195, color=(4, 244, 88), ): return False if not check_line_for_color( - vm_index, - x_1=169, - y_1=50, - x_2=189, - y_2=50, - color=(255, 222, 0), + vm_index, x_1=169, y_1=50, x_2=189, y_2=50, color=(255, 222, 0), ): return False if not check_line_for_color( - vm_index, - x_1=369, - y_1=63, - x_2=351, - y_2=71, - color=(228, 36, 36), + vm_index, x_1=369, y_1=63, x_2=351, y_2=71, color=(228, 36, 36), ): return False return True def wait_for_profile_page( - vm_index: int, - logger: Logger, - printmode: bool = False, + vm_index: int, logger: Logger, printmode: bool = False, ) -> Literal["restart", "good"]: """Waits for the profile page to load. @@ -806,42 +841,36 @@ def get_to_profile_page(vm_index: int, logger: Logger) -> Literal["restart", "go def check_for_trophy_reward_menu(vm_index) -> bool: - iar = screenshot(vm_index) + """Checks if the user is on the trophy reward menu. - pixels = [ - iar[592][172], - iar[617][180], - iar[607][190], - iar[603][200], - iar[596][210], - iar[593][220], - iar[600][230], - iar[610][235], - iar[623][246], - ] - colors = [ - [255, 184, 68], - [255, 175, 78], - [255, 175, 78], - [248, 239, 227], - [255, 187, 104], - [255, 176, 79], - [255, 187, 104], - [255, 175, 78], - [253, 135, 39], - ] + Args: + ---- + vm_index (int): The index of the virtual machine. - for i, pixel in enumerate(pixels): - if not pixel_is_equal(pixel, colors[i], tol=25): - return False + Returns: + ------- + bool: True if on the trophy reward menu, False otherwise. - return True + """ + if not region_is_color(vm_index, region=[172, 599, 22, 12], color=(78, 175, 255)): + return False + if not region_is_color(vm_index, region=[226, 601, 18, 10], color=(78, 175, 255)): + return False + + lines = [ + check_line_for_color( + vm_index, x_1=199, y_1=590, x_2=206, y_2=609, color=(255, 255, 255), + ), + check_line_for_color( + vm_index, x_1=211, y_1=590, x_2=220, y_2=609, color=(255, 255, 255), + ), + ] + + return all(lines) def handle_trophy_reward_menu( - vm_index, - logger: Logger, - printmode=False, + vm_index, logger: Logger, printmode=False, ) -> Literal["good"]: """Handles the trophy reward menu. @@ -870,38 +899,6 @@ def handle_trophy_reward_menu( return "good" -def check_for_megaknight_evolution_popup(vm_index): - iar = screenshot(vm_index) - pixels = [ - iar[585][170], - iar[596][208], - iar[599][180], - iar[610][200], - iar[613][190], - iar[590][210], - iar[595][220], - iar[600][230], - iar[605][240], - iar[615][250], - ] - colors = [ - [255, 186, 53], - [148, 133, 114], - [255, 175, 78], - [255, 178, 79], - [255, 175, 78], - [255, 187, 104], - [101, 76, 46], - [255, 255, 255], - [255, 176, 77], - [255, 141, 19], - ] - for i, c in enumerate(colors): - if not pixel_is_equal(c, pixels[i], tol=25): - return False - return True - - def wait_for_clash_main_menu(vm_index, logger: Logger, deadspace_click=True) -> bool: """Waits for the user to be on the clash main menu. Returns True if on main menu, prints the pixels if False then return False @@ -920,13 +917,6 @@ def wait_for_clash_main_menu(vm_index, logger: Logger, deadspace_click=True) -> time.sleep(2) continue - # handle getting stuck on megaknight evolution popup - if check_for_megaknight_evolution_popup(vm_index): - print("Handling megaknight evolution popup") - click(vm_index, 206, 601) - time.sleep(2) - continue - # click deadspace if deadspace_click and random.randint(0, 1) == 0: click( @@ -937,8 +927,12 @@ def wait_for_clash_main_menu(vm_index, logger: Logger, deadspace_click=True) -> time.sleep(1) time.sleep(1) - if check_if_on_clash_main_menu(vm_index) is not True: + out = check_if_on_clash_main_menu(vm_index) + if out is not True: print("Failed to get to clash main! Saw these pixels before restarting:") + # for pixel in out: + # print(pixel) + # print("\n") return False return True @@ -980,7 +974,7 @@ def check_if_on_path_of_legends_clash_main(vm_index): return True -def check_if_on_clash_main_menu(vm_index) -> bool: +def check_if_on_clash_main_menu(vm_index): """Checks if the user is on the clash main menu. Returns True if on main menu, False if not. """ @@ -1011,6 +1005,7 @@ def check_if_on_clash_main_menu(vm_index) -> bool: # if any pixel doesnt match the sentinel, then we're not on clash main for i, pixel in enumerate(pixels): if not pixel_is_equal(pixel, colors[i], tol=35): + # return pixels return False # if all pixels are good, we're on clash main @@ -1018,9 +1013,7 @@ def check_if_on_clash_main_menu(vm_index) -> bool: def get_to_card_page_from_clash_main( - vm_index: int, - logger: Logger, - printmode: bool = False, + vm_index: int, logger: Logger, printmode: bool = False, ) -> Literal["restart", "good"]: """Clicks on the card page icon from the clash main screen and waits until the card page is loaded. If the card page is not loaded within 60 seconds, returns "restart". @@ -1045,11 +1038,16 @@ def get_to_card_page_from_clash_main( else: logger.log("Getting to card page from clash main") + # change to trophy road + click(vm_index, 286, 391) + time.sleep(2.5) + + click(vm_index, 69, 293) + time.sleep(2.5) + # click card page icon click( - vm_index, - CARD_PAGE_ICON_FROM_CLASH_MAIN[0], - CARD_PAGE_ICON_FROM_CLASH_MAIN[1], + vm_index, CARD_PAGE_ICON_FROM_CLASH_MAIN[0], CARD_PAGE_ICON_FROM_CLASH_MAIN[1], ) time.sleep(2.5) @@ -1060,9 +1058,7 @@ def get_to_card_page_from_clash_main( return "restart" click( - vm_index, - CARD_PAGE_ICON_FROM_CARD_PAGE[0], - CARD_PAGE_ICON_FROM_CARD_PAGE[1], + vm_index, CARD_PAGE_ICON_FROM_CARD_PAGE[0], CARD_PAGE_ICON_FROM_CARD_PAGE[1], ) time.sleep(3) @@ -1095,100 +1091,79 @@ def check_if_on_underleveled_card_page(vm_index): return True -def check_if_on_card_page(vm_index) -> bool: - def check_if_on_card_page2(iar): - pixels = [ - iar[441][58], - iar[191][18], - iar[211][390], - iar[435][325], - iar[102][59], - iar[109][56], - iar[116][55], - ] - colors = [ - [232, 0, 248], - [105, 43, 1], - [105, 44, 1], - [249, 186, 100], - [255, 255, 255], - [255, 255, 255], - [255, 255, 255], - ] - for i, p in enumerate(pixels): - if not pixel_is_equal(colors[i], p, tol=15): - return False +def check_if_on_goblin_mode_card_page(vm_index): + iar = screenshot(vm_index) + pixels = [ + iar[108][175], + iar[112][189], + iar[103][254], + iar[109][295], + iar[446][54], + iar[446][64], + iar[444][49], + iar[14][210], + iar[14][325], + ] + # for p in pixels:print(p) + colors = [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [223, 1, 237], + [228, 0, 243], + [186, 8, 190], + [255, 255, 255], + [255, 255, 255], + ] - return True + for i, p in enumerate(pixels): + if not pixel_is_equal(colors[i], p, tol=15): + return False - def check_if_on_path_of_legends_mode_card_page(iar): - pixels = [ - iar[108][175], - iar[112][189], - iar[103][254], - iar[109][295], - iar[446][54], - iar[446][64], - iar[444][49], - iar[14][210], - iar[14][325], - ] - colors = [ - [186, 105, 143], - [254, 254, 254], - [229, 188, 206], - [213, 175, 191], - [224, 1, 237], - [229, 0, 244], - [187, 7, 191], - [255, 255, 255], - [255, 255, 255], - ] - - for i, p in enumerate(pixels): - if not pixel_is_equal(colors[i], p, tol=15): - return False + return True - return True - def check_if_on_goblin_mode_card_page(iar): - pixels = [ - iar[108][175], - iar[112][189], - iar[103][254], - iar[109][295], - iar[446][54], - iar[446][64], - iar[444][49], - iar[14][210], - iar[14][325], - ] - colors = [ - [255, 255, 255], - [255, 255, 255], - [255, 255, 255], - [255, 255, 255], - [223, 1, 237], - [228, 0, 243], - [186, 8, 190], - [255, 255, 255], - [255, 255, 255], - ] - - for i, p in enumerate(pixels): - if not pixel_is_equal(colors[i], p, tol=15): - return False +def check_if_on_path_of_legends_mode_card_page(vm_index): + iar = screenshot(vm_index) + pixels = [ + iar[108][175], + iar[112][189], + iar[103][254], + iar[109][295], + iar[446][54], + iar[446][64], + iar[444][49], + iar[14][210], + iar[14][325], + ] + colors = [ + [186, 105, 143], + [254, 254, 254], + [229, 188, 206], + [213, 175, 191], + [224, 1, 237], + [229, 0, 244], + [187, 7, 191], + [255, 255, 255], + [255, 255, 255], + ] - return True + for i, p in enumerate(pixels): + if not pixel_is_equal(colors[i], p, tol=15): + print(i) + return False - iar = screenshot(vm_index) - if check_if_on_card_page2(iar): - return True - if check_if_on_goblin_mode_card_page(iar): - return True - if check_if_on_path_of_legends_mode_card_page(iar): + return True + + +def check_if_on_card_page(vm_index) -> bool: + if check_if_on_goblin_mode_card_page( + vm_index, + ) or check_if_on_path_of_legends_mode_card_page(vm_index): return True + iar = screenshot(vm_index) pixels = [ iar[433][58], iar[101][55], @@ -1250,9 +1225,8 @@ def get_to_challenges_tab_from_main(vm_index, logger) -> Literal["restart", "goo def handle_clash_main_tab_notifications( - vm_index, - logger: Logger, -) -> bool: + vm_index, logger: Logger, +) -> Literal["restart", "good"]: """Clicks on the card, shop, and challenges tabs in the Clash Main menu to handle notifications. Args: @@ -1300,9 +1274,13 @@ def handle_clash_main_tab_notifications( print("Getting to events tab...") while not check_for_events_page(vm_index): print("Still not on events page...") + click(vm_index, 408, 600) + handle_war_popup_pages(vm_index, logger) + time.sleep(1) + print("On events page") # spam click shop page at the leftmost location, wait a little bit @@ -1365,16 +1343,17 @@ def check_for_events_page(vm_index): [154, 119, 80], ] + # for p in pixels:print(p) + for i, p in enumerate(pixels): + # print(p) if not pixel_is_equal(colors[i], p, tol=15): return False return True def wait_for_clash_main_challenges_tab( - vm_index, - logger: Logger, - printmode=False, + vm_index, logger: Logger, printmode=False, ) -> Literal["restart", "good"]: """Waits for the Clash Main menu to be on the challenges tab. @@ -1451,20 +1430,10 @@ def check_if_on_clash_main_shop_page(vm_index) -> bool: lines = [ check_line_for_color( - vm_index, - x_1=393, - y_1=7, - x_2=414, - y_2=29, - color=(44, 144, 21), + vm_index, x_1=393, y_1=7, x_2=414, y_2=29, color=(44, 144, 21), ), check_line_for_color( - vm_index, - x_1=48, - y_1=593, - x_2=83, - y_2=594, - color=(102, 236, 56), + vm_index, x_1=48, y_1=593, x_2=83, y_2=594, color=(102, 236, 56), ), ] @@ -1472,8 +1441,7 @@ def check_if_on_clash_main_shop_page(vm_index) -> bool: def wait_for_clash_main_shop_page( - vm_index, - logger: Logger, + vm_index, logger: Logger, ) -> Literal["restart", "good"]: """Wait for the bot to navigate to the main shop page in the Clash of Clans game. @@ -1502,9 +1470,7 @@ def wait_for_clash_main_shop_page( def get_to_activity_log( - vm_index: int, - logger: Logger, - printmode: bool = False, + vm_index: int, logger: Logger, printmode: bool = False, ) -> Literal["restart", "good"]: """Navigates to the activity log page in the Clash of Clans game. @@ -1564,9 +1530,7 @@ def get_to_activity_log( def wait_for_battle_log_page( - vm_index, - logger: Logger, - printmode=False, + vm_index, logger: Logger, printmode=False, ) -> Literal["restart", "good"]: """Waits for the battle log page to appear. @@ -1618,6 +1582,7 @@ def check_if_on_battle_log_page(vm_index) -> bool: iar[62][92], iar[77][316], ] + # for p in pixels:print(p) colors = [ [255, 255, 255], [255, 255, 255], @@ -1680,9 +1645,7 @@ def check_if_on_clash_main_burger_button_options_menu(vm_index) -> bool: def wait_for_clash_main_burger_button_options_menu( - vm_index: int, - logger: Logger, - printmode: bool = False, + vm_index: int, logger: Logger, printmode: bool = False, ) -> Literal["restart", "good"]: """Waits for the virtual machine to be on the clash main burger button options menu. @@ -1720,98 +1683,5 @@ def wait_for_clash_main_burger_button_options_menu( return "good" -def check_if_on_collection_page(vm_index) -> bool: - iar = screenshot(vm_index) - - queens_mode_colors = [ - [41, 104, 169], - [42, 104, 168], - [255, 255, 255], - [183, 197, 214], - [39, 101, 164], - [42, 105, 170], - [43, 106, 171], - [43, 104, 168], - [28, 77, 132], - [22, 58, 102], - ] - trophy_mode_colors = [ - [211, 159, 45], - [203, 134, 41], - [255, 255, 255], - [217, 202, 181], - [199, 132, 40], - [207, 141, 47], - [205, 139, 44], - [201, 135, 42], - [149, 98, 29], - [178, 104, 14], - ] - - legends2_mode_colors = [ - [251, 215, 231], - [248, 211, 227], - [255, 255, 255], - [235, 218, 226], - [247, 210, 226], - [254, 218, 234], - [254, 218, 234], - [254, 217, 233], - [208, 159, 182], - [207, 159, 179], - ] - - pixels = [ - iar[53][220], - iar[60][230], - iar[70][240], - iar[65][260], - iar[60][280], - iar[55][290], - iar[57][300], - iar[59][310], - iar[62][320], - iar[78][335], - ] - - if ( - pixels_match_colors(pixels, queens_mode_colors) - or pixels_match_colors(pixels, trophy_mode_colors) - or pixels_match_colors(pixels, legends2_mode_colors) - ): - return True - - return False - - -def get_to_collections_page(vm_index) -> bool: - # starts on clash main - if not check_if_on_clash_main_menu(vm_index): - print("Not on clash main for get_to_magic_items_page()!") - return False - - # click card page - card_page_coords = [100, 600] - click(vm_index, card_page_coords[0], card_page_coords[1]) - time.sleep(1) - - cycle_card_page_coord = [135, 590] - - timeout = 30 # s - start_time = time.time() - while not check_if_on_collection_page(vm_index): - # timeout check - if time.time() - start_time > timeout: - print("Timed out waiting for collection page") - return False - - click(vm_index, cycle_card_page_coord[0], cycle_card_page_coord[1]) - time.sleep(1) - - return True - - if __name__ == "__main__": - print("\n\n\n\n\n\n") - while 1: - print(check_if_on_collection_page(1)) + print(check_if_on_path_of_legends_mode_card_page(12)) diff --git a/src/pyclashbot/bot/open_chests_state.py b/src/pyclashbot/bot/open_chests_state.py index 318eb8289..daa33e849 100644 --- a/src/pyclashbot/bot/open_chests_state.py +++ b/src/pyclashbot/bot/open_chests_state.py @@ -19,7 +19,7 @@ UNLOCK_CHEST_BUTTON_COORD = (207, 412) QUEUE_CHEST_BUTTON_COORD = (314, 357) -CLASH_MAIN_DEADSPACE_COORD = (20, 520) +CLASH_MAIN_DEADSPACE_COORD = (20, 450) CHEST_OPENING_DEADSPACE_CLICK_TIMEOUT = 40 # s @@ -39,25 +39,24 @@ def open_chests_state(vm_index: int, logger: Logger, next_state: str) -> str: """ open_chests_start_time = time.time() + logger.add_chest_unlock_attempt() logger.change_status(status="Opening chests state") # handle being on trophy road meu - print('Checking for trophy reward menu...') if check_for_trophy_reward_menu(vm_index): - print('Found trophy reward menu\nHandling it') handle_trophy_reward_menu(vm_index, logger) time.sleep(3) - else: - print('No trophy reward menu found') # if not on clash_main, print the pixels that the box sees, then restart - print('Checking if on clash main before doing chest opening') clash_main_check = check_if_on_clash_main_menu(vm_index) if clash_main_check is not True: logger.log("Not on clashmain for the start of open_chests_state()") + # logger.log("These are the pixels the bot saw after failing to find clash main:") + # for pixel in clash_main_check: + # logger.log(f" {pixel}") + return "restart" - #clear main tab notifications logger.change_status( status="Handling obstructing notifications before opening chests", ) diff --git a/src/pyclashbot/bot/request_state.py b/src/pyclashbot/bot/request_state.py index 268bdf680..1e52b9971 100644 --- a/src/pyclashbot/bot/request_state.py +++ b/src/pyclashbot/bot/request_state.py @@ -27,8 +27,6 @@ ) from pyclashbot.utils.logger import Logger -CLASH_MAIN_DEADSPACE_COORD = (20, 520) - def find_request_button(vm_index, logger: Logger): """Finds the location of the request button on the screen. @@ -79,6 +77,7 @@ def request_state(vm_index, logger: Logger, next_state: str) -> str: """ logger.change_status(status="Doing request state!") + logger.add_request_attempt() # if not on main: return clash_main_check = check_if_on_clash_main_menu(vm_index) @@ -92,7 +91,7 @@ def request_state(vm_index, logger: Logger, next_state: str) -> str: return "restart" # if logger says we're not in a clan, check if we are in a clan - if logger.is_in_clan() is False: + if logger.in_a_clan is False: logger.change_status("Checking if in a clan before requesting") in_a_clan_return = request_state_check_if_in_a_clan(vm_index, logger) if in_a_clan_return == "restart": @@ -104,18 +103,20 @@ def request_state(vm_index, logger: Logger, next_state: str) -> str: if not in_a_clan_return: return next_state else: - print(f"Logger's in_a_clan value is: {logger.is_in_clan()} so skipping check") + print(f"Logger's in_a_clan value is: {logger.in_a_clan} so skipping check") # if in a clan, update logger's in_a_clan value logger.update_in_a_clan_value(True) - print(f"Set Logger's in_a_clan value to: {logger.is_in_clan()}!") + print(f"Set Logger's in_a_clan value to: {logger.in_a_clan}!") # get to clan page logger.change_status("Getting to clan tab to request a card") - if get_to_clan_tab_from_clash_main(vm_index, logger) is False: + if get_to_clan_tab_from_clash_main(vm_index, logger) == "restart": logger.change_status(status="ERROR 74842744443 Not on clan tab") return "restart" + logger.update_time_of_last_request(time.time()) + # check if request exists if check_if_can_request_wrapper(vm_index): # do request @@ -135,10 +136,6 @@ def request_state(vm_index, logger: Logger, next_state: str) -> str: def do_random_scrolling_in_request_page(vm_index, logger, scrolls) -> None: logger.change_status(status="Doing random scrolling in request page") - # scroll up to top - for _ in range(3): - scroll_up_on_left_side_of_screen(vm_index) - for _ in range(scrolls): scroll_down_in_request_page(vm_index) time.sleep(1) @@ -158,15 +155,14 @@ def count_scrolls_in_request_page(vm_index) -> int: print(f"One scroll down. Count is {scrolls}") scroll_down_in_request_page(vm_index) scrolls += 1 - time.sleep(2) + time.sleep(1) # if taken too much time, return 5 if time.time() - start_time > timeout: return 5 # close request screen with deadspace click - click(vm_index, 15, 300, clicks=3) - time.sleep(0.1) + click(vm_index, 15, 300, clicks=3, interval=1) # reopen request page click(vm_index=vm_index, x_coord=77, y_coord=536) @@ -176,45 +172,13 @@ def count_scrolls_in_request_page(vm_index) -> int: def check_if_can_scroll_in_request_page(vm_index) -> bool: - iar = numpy.asarray(screenshot(vm_index)) - pixels = [ - iar[533][58], - iar[533][63], - iar[533][60], - iar[533][90], - iar[533][120], - iar[533][150], - iar[533][180], - iar[533][210], - iar[533][240], - iar[533][270], - iar[533][300], - iar[529][337], - ] - colors = [ - [241, 235, 222], - [241, 235, 222], - [241, 235, 222], - [241, 235, 222], - [241, 235, 222], - [241, 235, 222], - [241, 235, 222], - [241, 235, 222], - [241, 235, 222], - [241, 235, 222], - [241, 235, 222], - [241, 235, 222], - ] - - for i, c in enumerate(colors): - if not pixel_is_equal(c, pixels[i], tol=10): - return True + if not region_is_color(vm_index, region=[64, 500, 293, 55], color=(222, 235, 241)): + return True return False def request_state_check_if_in_a_clan( - vm_index, - logger: Logger, + vm_index, logger: Logger, ) -> bool | Literal["restart"]: # if not on clash main, reutnr if check_if_on_clash_main_menu(vm_index) is not True: @@ -236,7 +200,7 @@ def request_state_check_if_in_a_clan( logger.change_status("Not in a clan, so can't request!") # click deadspace to leave - click(vm_index, CLASH_MAIN_DEADSPACE_COORD[0], CLASH_MAIN_DEADSPACE_COORD[1]) + click(vm_index, 15, 300) if wait_for_clash_main_menu(vm_index, logger) is False: logger.change_status( status="Error 87258301758939 Failure with wait_for_clash_main_menu", @@ -274,45 +238,6 @@ def request_state_check_pixels_for_clan_flag(vm_index) -> bool: return False -def click_random_requestable_card(vm_index) -> bool: - def make_coord_list(x_range,y_range): - coords = [] - for x in range(x_range[0],x_range[1]): - for y in range(y_range[0],y_range[1]): - c = [x,y] - coords.append(c) - - random.shuffle(coords) - return coords - - def is_valid_pixel(pixel): - def is_greyscale_pixel(pixel): - #if all the values are wihtin 5 of eachother, it's greyscale - if abs(pixel[0] - pixel[1]) < 5 and abs(pixel[1] - pixel[2]) < 5: - return True - - return False - - if is_greyscale_pixel(pixel): - return False - - if pixel_is_equal(pixel,[222,235,241],tol=20): - return False - - return True - - #check pixels in the request card grid region - iar = screenshot(vm_index) - for coord in make_coord_list((69,356),(213,557)): - pixel = iar[coord[1]][coord[0]] - #if the pixel indicates requestable, click it, return True - if is_valid_pixel(pixel): - click(vm_index,coord[0],coord[1]) - return True - - #fail return - return False - def do_request(vm_index, logger: Logger) -> bool: logger.change_status(status="Initiating request process") @@ -330,9 +255,7 @@ def do_request(vm_index, logger: Logger) -> bool: # Perform random scrolling in the request page do_random_scrolling_in_request_page( - vm_index=vm_index, - logger=logger, - scrolls=random_scroll_amount, + vm_index=vm_index, logger=logger, scrolls=random_scroll_amount, ) # Timeout settings for random clicking @@ -355,17 +278,11 @@ def do_request(vm_index, logger: Logger) -> bool: # Click on a random card logger.change_status(status="Clicking a random card to request") - - click_try_limit = 10 - click_tries = 0 - while click_random_requestable_card(vm_index) is False: - print('Failed to click an upgradable card.') - click_tries += 1 - if click_tries>click_try_limit: - logger.change_status('Failed to click an upgradable card. too many times') - return False - - + click( + vm_index=vm_index, + x_coord=random.randint(a=67, b=358), + y_coord=random.randint(a=211, b=547), + ) time.sleep(3) # Attempt to find the request button @@ -395,6 +312,10 @@ def check_if_can_request_wrapper(vm_index) -> bool: print("Detected trade cards icon") return False + if check_for_trade_cards_icon_2(vm_index): + print("Detected trade cards icon") + return False + if check_if_can_request_3(vm_index): return True @@ -456,7 +377,7 @@ def check_for_epic_sunday_icon(vm_index): ] for i, p in enumerate(pixels): - + # print(p) if not pixel_is_equal(colors[i], p, tol=10): return False return True @@ -479,39 +400,35 @@ def check_if_can_request_2(vm_index) -> bool: def check_for_trade_cards_icon(vm_index) -> bool: - iar = numpy.asarray(screenshot(vm_index)) - pixels = [ - iar[518][38], - iar[520][42], - iar[525][75], - iar[526][45], - iar[527][50], - iar[529][60], - iar[530][60], - iar[535][70], - iar[537][80], - iar[540][90], - iar[541][100], - iar[543][104], + lines = [ + check_line_for_color( + vm_index, x_1=33, y_1=502, x_2=56, y_2=502, color=(47, 69, 105), + ), + check_line_for_color( + vm_index, x_1=56, y_1=507, x_2=108, y_2=506, color=(253, 253, 203), + ), + check_line_for_color( + vm_index, x_1=37, y_1=515, x_2=125, y_2=557, color=(255, 188, 42), + ), ] - colors = [ - [64, 46, 36], - [46, 191, 255], - [73, 111, 129], - [78, 185, 232], - [255, 238, 237], - [224, 204, 205], - [219, 199, 200], - [37, 83, 104], - [82, 112, 125], - [254, 255, 255], - [250, 253, 255], - [43, 189, 253], - ] - # for p in pixels:print(p) - for i, c in enumerate(colors): - if not pixel_is_equal(c, pixels[i], tol=10): - return False + + return all(lines) + + +def check_for_trade_cards_icon_2(vm_index): + if not check_line_for_color(vm_index, 67, 524, 74, 534, (255, 255, 254)): + return False + if not check_line_for_color(vm_index, 90, 523, 91, 534, (255, 255, 254)): + return False + if not check_line_for_color(vm_index, 97, 536, 102, 543, (255, 253, 250)): + return False + + if not region_is_color(vm_index, [50, 530, 4, 8], (212, 228, 255)): + return False + if not region_is_color(vm_index, [106, 523, 4, 8], (255, 200, 80)): + return False + if not region_is_color(vm_index, [104, 536, 12, 8], (255, 188, 42)): + return False return True @@ -525,4 +442,6 @@ def check_if_can_request_3(vm_index): if __name__ == "__main__": - print(request_state(1, Logger(), "next_state")) + # vm_index = 12 + # logger = Logger(None) + pass diff --git a/src/pyclashbot/bot/season_shop_offers.py b/src/pyclashbot/bot/season_shop_offers.py index 5ba805542..86cb187df 100644 --- a/src/pyclashbot/bot/season_shop_offers.py +++ b/src/pyclashbot/bot/season_shop_offers.py @@ -6,7 +6,6 @@ from pyclashbot.bot.nav import ( check_if_on_clash_main_menu, get_to_challenges_tab_from_main, - wait_for_clash_main_menu, ) from pyclashbot.detection.image_rec import pixel_is_equal from pyclashbot.memu.client import click, screenshot, scroll @@ -143,15 +142,10 @@ def buy_season_shop_offers(vm_index, logger: Logger): return True -def get_to_clash_main_from_event_page(vm_index, logger: Logger) -> bool: - cr_main_coord = (175, 600) - click(vm_index, *cr_main_coord) - time.sleep(3) - - return wait_for_clash_main_menu(vm_index, logger) - - def collect_season_shop_offers_state(vm_index: int, logger: Logger, next_state: str): + # increment attempts + logger.increment_season_shop_buys_attempts() + # if not on main, return 'restart' if not check_if_on_clash_main_menu(vm_index): logger.change_status("Not on clash main for collect_season_shop_offers_state()") @@ -165,7 +159,6 @@ def collect_season_shop_offers_state(vm_index: int, logger: Logger, next_state: logger.change_status( f"Cant collect season shop offers. Returning next state as : {next_state}", ) - get_to_clash_main_from_event_page(vm_index, logger) return next_state if not buy_season_shop_offers(vm_index, logger): diff --git a/src/pyclashbot/bot/states.py b/src/pyclashbot/bot/states.py index 27116a11d..8cea121cf 100644 --- a/src/pyclashbot/bot/states.py +++ b/src/pyclashbot/bot/states.py @@ -1,7 +1,6 @@ """time module for timing functions and controling pacing""" import time -import random from pyclashbot.bot.account_switching import switch_accounts from pyclashbot.bot.bannerbox import collect_bannerbox_rewards_state @@ -23,7 +22,7 @@ from pyclashbot.bot.request_state import request_state from pyclashbot.bot.season_shop_offers import collect_season_shop_offers_state from pyclashbot.bot.trophy_road_rewards import collect_trophy_road_rewards_state -from pyclashbot.bot.magic_items import spend_magic_items_state +from pyclashbot.bot.upgrade_all_cards import upgrade_all_cards_state from pyclashbot.bot.upgrade_state import upgrade_cards_state from pyclashbot.bot.war_state import war_state from pyclashbot.memu.client import click @@ -37,171 +36,12 @@ mode_used_in_1v1 = None -CLASH_MAIN_DEADSPACE_COORD = (20, 520) - - -def get_render_mode(job_list): - render_mode = "opengl" - if job_list["directx_toggle"]: - render_mode = "directx" - return render_mode - - -class StateHistory: - def __init__(self, logger): - self.time_history_string_list = [] - self.logger = logger - - # This increment time is hard-coded to be as - # low as possible while not spamming slow states - - self.state2time_increment = { - # "account_switch": 0.8, #'state':increment in hours, - "account_switch": 0.01, #'state':increment in hours, - "open_chests": 1, #'state':increment in hours, - "level_up_chests": 0, #'state':increment in hours, - "upgrade": 0, #'state':increment in hours, - "trophy_rewards": 0.25, #'state':increment in hours, - "request": 1, #'state':increment in hours, - "donate": 1, #'state':increment in hours, - "shop_buy": 2, #'state':increment in hours, - "bannerbox": 1, #'state':increment in hours, - "daily_rewards": 0, #'state':increment in hours, - "battlepass_rewards": 0.25, #'state':increment in hours, - "card_mastery": 0, #'state':increment in hours, - "season_shop": 2, #'state':increment in hours, - "war": 0.25, #'state':increment in hours, - "magic_items": 1, - } - self.randomize_state2time_increment() - - def randomize_state2time_increment(self): - percent_diff = 40 - for state, time_increment in self.state2time_increment.items(): - adjustment_factor = ( - random.randint(100 - percent_diff, 100 + percent_diff) / 100 - ) - new_value = time_increment * adjustment_factor - self.state2time_increment[state] = new_value - - def print_time_increments(self): - def hours2readable(hours): - def format_digit(digit): - digit = str(digit) - while len(digit) < 2: - digit = "0" + str(digit) - - return str(digit) - - remainder = hours * 60 * 60 - - hours = int(remainder // 3600) - remainder = remainder % 3600 - - minutes = int(remainder // 60) - remainder = remainder % 60 - - seconds = int(remainder) - - return ( - f"{format_digit(hours)}:{format_digit(minutes)}:{format_digit(seconds)}" - ) - - for state, time_increment in self.state2time_increment.items(): - print("{:>20} : {}".format(state, hours2readable(time_increment))) - - def print(self): - print("State history:") - for i, line in enumerate(self.time_history_string_list): - print("\t", i, line) - print("\n") - - def add_state(self, state): - time_history_string = ( - f"{state} {time.time()} {int(self.logger.current_account)}" - ) - self.time_history_string_list.append(time_history_string) - - def get_time_of_last_state(self, state: str) -> int: - most_recent_time = -1 - for line in self.time_history_string_list: - # filter by state - if state in line: - # split line - try: - # split by account index - state, time, this_account_index = line.split(" ") - time = float(time) - this_account_index = int(this_account_index) - # account_switch state ignores account index filter - if state != "account_switch" and int(this_account_index) != int( - self.logger.current_account - ): - continue - - # handling negative time for whatever reason - if time > most_recent_time: - most_recent_time = time - except Exception as e: - print( - f"Got an expcetion in StateHistory.get_time_of_last_state()\n{e}" - ) - pass - - return int(most_recent_time) - - def state_is_ready(self, state: str) -> bool: - def to_wrap(): - # if the state isnt in the state time increment dictionary, return True - if state not in self.state2time_increment: - print( - f"The time increment for {state} isn't specified, so defaulting to True (ready)" - ) - return True - - # get the time of the last state - last_time = self.get_time_of_last_state(state) - - # if the last time is -1, then the state has never been run before - if last_time == -1: - print(f"{state} has never been run before, so it is ready") - return True - - # retrieve the time increment for this state - time_increment = self.state2time_increment[state] - - # convert the time increment from hours to seconds - time_increment = time_increment * 60 * 60 - - # time since last state - time_since_last_state = time.time() - last_time - print( - f"It's been {str(time_since_last_state)[:5]}s since this state has been ran" - ) - - # if the time since the last state is greater than the time increment, return True - if time_since_last_state > time_increment: - print(f"{state} is ready to run") - return True - - # otherwise - print(f"{state} is not ready to run") - return False - - # add ready states to history because they always happen after True returns - if to_wrap(): - self.add_state(state) - return True - - return False - def state_tree( vm_index, logger: Logger, state, job_list, - state_history: StateHistory, ) -> str: """Method to handle and loop between the various states of the bot""" global mode_used_in_1v1 @@ -222,9 +62,11 @@ def state_tree( if job_list["memu_attach_mode_toggle"]: start_memu_dock_mode() + logger.set_total_accounts(len(job_list["random_account_switch_list"])) + next_state = "account_switch" - restart_emulator(logger, get_render_mode(job_list)) + restart_emulator(logger) logger.log( f"Emulator boot sequence took {str(time.time() - start_time)[:5]} seconds", @@ -266,18 +108,21 @@ def state_tree( break # Successfully handled starting battle or end-of-battle scenario if battle_start_result == "restart": # Need to restart the process due to issues detected - return state_tree(vm_index, logger, "restart", job_list, state_history) + return state_tree(vm_index, logger, "restart", job_list) # click deadspace - click( - vm_index, CLASH_MAIN_DEADSPACE_COORD[0], CLASH_MAIN_DEADSPACE_COORD[1] - ) + click(vm_index, 5, 350) + + # logger.log("Not on clash main") + # logger.log("Pixels are none: ") + # for p in clash_main_check: + # logger.log(p) if check_if_on_clash_main_menu(vm_index) is not True: logger.log("Clash main wait timed out! These are the pixels it saw:") # for p in clash_main_check: # logger.log(p) - return state_tree(vm_index, logger, "restart", job_list, state_history) + return state_tree(vm_index, logger, "restart", job_list) logger.log('Detected clash main at the end of "restart" state.') logger.log( @@ -295,36 +140,39 @@ def state_tree( logger.log("Account switching isn't toggled. Skipping this state") return next_state - # if job not ready, return next state - if state_history.state_is_ready("account_switch") is False: - logger.log("Account switching isn't ready. Skipping this state") + # if job not ready, reutrn next state + if not logger.check_if_can_switch_account( + job_list["account_switching_increment_user_input"], + ): + logger.log("Account switching job isn't ready. Skipping this state") return next_state - # see how many accounts there are in the toggle - account_total = job_list["account_switch_count"] + account_total = job_list["account_switching_slider"] + logger.log(f"Doing switch #{job_list['next_account']} of {account_total}") - # see what account we should use right now - next_account_index = logger.get_next_account(account_total) + accout_index = job_list["next_account"] - # switch to that account if ( switch_accounts( - vm_index, - logger, - next_account_index, + vm_index, logger, job_list["random_account_switch_list"][accout_index], ) is False ): - logger.change_status( - f"Failed to switch to account #{next_account_index}. Restarting..." - ) return "restart" - # set current account - logger.current_account = int(next_account_index) + # increment next account iteration + job_list["next_account"] += 1 + if job_list["next_account"] >= job_list["account_switching_slider"]: + job_list["next_account"] = 0 - # add this account to logger's account history object - logger.add_account_to_account_history(next_account_index) + logger.log( + f"Next account is {job_list['next_account']} / {job_list['account_switching_slider']}", + ) + + # update current account # to GUI + logger.change_current_account( + job_list["random_account_switch_list"][accout_index], + ) return next_state @@ -338,9 +186,15 @@ def state_tree( return next_state # if all chests are available, skip this increment user input check - if state_history.state_is_ready(state) is False: - logger.log(f"{state} isn't ready. Skipping this state...") - return next_state + if not job_list["skip_fight_if_full_chests_user_toggle"]: + logger.log('"skip_fight_if_full_chests_user_toggle" is off') + # if job not ready, skip this state + logger.log('Checking if "open_chests_increment_user_input" is ready') + if not logger.check_if_can_open_chests( + job_list["open_chests_increment_user_input"], + ): + logger.log("Can't open chests at this time, skipping this state") + return next_state # run this state logger.log('Open chests is toggled and ready. Running "open_chests_state()"') @@ -359,9 +213,12 @@ def state_tree( logger.log("level_up_chest_user_toggle is off, skipping this state") return next_state - # if job not ready, go next state - if state_history.state_is_ready(state) is False: - logger.log(f"{state} isn't ready. Skipping this state...") + # if job not ready, skip this state + logger.log('Checking if "open_chests_increment_user_input" is ready') + if not logger.check_if_can_collect_level_up_chest( + job_list["level_up_chest_increment_user_input"], + ): + logger.log("Can't open level up chest at this time, skipping this state") return next_state # run this state @@ -378,55 +235,88 @@ def state_tree( logger.log("deck randomization isn't toggled. skipping this state") return next_state - # make sure there's a relevent job toggled, else just skip deck randomization - if ( - not job_list["trophy_road_1v1_battle_user_toggle"] - and not job_list["goblin_queens_journey_1v1_battle_user_toggle"] - and not job_list["path_of_legends_1v1_battle_user_toggle"] - and not job_list["upgrade_user_toggle"] - and not job_list["2v2_battle_user_toggle"] + # if randomize deck isn't ready, return next state + if not logger.check_if_can_randomize_deck( + job_list["deck_randomization_increment_user_input"], ): - print( - "No fight jobs, or card jobs are even toggled, so skipping random deck state." - ) + logger.log("deck randomization isn't ready. skipping this state") return next_state return randomize_deck_state(vm_index, logger, next_state) - if state == "upgrade": # --> trophy_rewards - next_state = "trophy_rewards" + if state == "upgrade": # --> upgrade_all + next_state = "upgrade_all" # if job not selected, return next state if not job_list["upgrade_user_toggle"]: logger.log("Upgrade user toggle is off, skipping this state") return next_state - # if job not ready, go next state - if state_history.state_is_ready(state) is False: - logger.log(f"{state} isn't ready. Skipping this state...") + # if job not ready, reutrn next state + if not logger.check_if_can_card_upgrade( + job_list["card_upgrade_increment_user_input"], + ): + logger.log("Upgrade state isn't ready, skipping this state") return next_state # return output of this state return upgrade_cards_state(vm_index, logger, next_state) + if state == "upgrade_all": # --> trophy_rewards + print("Running upgrade_all state") + next_state = "trophy_rewards" + + # if 'upgrade_user_toggle' is toggled on, return next state + print("Checking if upgrade_user_toggle job is toggled") + if job_list["upgrade_user_toggle"]: + logger.change_status( + "Regular upgrade is toggled. Skipping 'UPGRADE ALL' state", + ) + return next_state + + # if job isnt selected, just return the next state + print("Checking if upgrade_all_cards_user_toggle job is toggled") + if not job_list["upgrade_all_cards_user_toggle"]: + logger.change_status( + "Upgrade all cards is not toggled. Skipping this state", + ) + return next_state + + # if job is available, increment attempts, run the state + print("Checking if upgrade_all job is ready...") + if logger.check_if_can_card_upgrade( + job_list["card_upgrade_increment_user_input"], + ): + logger.change_status("Upgrade all cards is ready!") + logger.add_card_upgrade_attempt() + return upgrade_all_cards_state(vm_index, logger, next_state) + + # else just return next state + logger.change_status("Upgrade all cards isn't ready!") + return next_state + if state == "trophy_rewards": # --> request next_state = "request" # if job isnt selected, just return the next state if not job_list["trophy_road_rewards_user_toggle"]: - logger.log( + logger.change_status( "Trophy rewards collection is not toggled. Skipping this state", ) return next_state - # if job not ready, go next state - if state_history.state_is_ready(state) is False: - logger.log(f"{state} isn't ready. Skipping this state...") - return next_state + # if job is available, increment attempts, run the state + if logger.check_if_can_collect_trophy_road_rewards( + job_list["trophy_road_reward_increment_user_input"], + ): + logger.change_status("Trophy rewards collection is ready!") + logger.add_trophy_reward_collect_attempt() + return collect_trophy_road_rewards_state(vm_index, logger, next_state) - # run the state - logger.log("Trophy rewards collection is ready!") - return collect_trophy_road_rewards_state(vm_index, logger, next_state) + # else just return next state + logger.change_status("Trophy rewards collection isn't ready!") + + return next_state if state == "request": # --> donate next_state = "donate" @@ -436,9 +326,9 @@ def state_tree( logger.log("Request job isn't toggled. Skipping") return next_state - # if job not ready, go next state - if state_history.state_is_ready(state) is False: - logger.log(f"{state} isn't ready. Skipping this state...") + # if job not ready, reutrn next state + if not logger.check_if_can_request(job_list["request_increment_user_input"]): + logger.log("Request job isn't ready. Skipping") return next_state # return output of this state @@ -452,17 +342,14 @@ def state_tree( logger.log("Donate job isn't toggled. Skipping") return next_state - # if job not ready, go next state - if state_history.state_is_ready(state) is False: - logger.log(f"{state} isn't ready. Skipping this state...") + # if job not ready, reutrn next state + if not logger.check_if_can_donate(job_list["donate_increment_user_input"]): + logger.log("Donate job isn't ready. Skipping") return next_state # return output of this state return donate_cards_state( - vm_index, - logger, - next_state, - job_list["free_donate_toggle"], + vm_index, logger, next_state, job_list["free_donate_toggle"], ) if state == "shop_buy": # --> bannerbox @@ -476,9 +363,9 @@ def state_tree( logger.log("Free neither free, not gold offer buys toggled") return next_state - # if job not ready, go next state - if state_history.state_is_ready(state) is False: - logger.log(f"{state} isn't ready. Skipping this state...") + # if job not ready, reutrn next state + if not logger.check_if_can_shop_buy(job_list["shop_buy_increment_user_input"]): + logger.log("Free shop_buy isn't ready") return next_state # return output of this state @@ -492,17 +379,10 @@ def state_tree( if state == "bannerbox": # --> daily_rewards next_state = "daily_rewards" - - # if not in job list, go next state if not job_list["open_bannerbox_user_toggle"]: logger.log("Bannerbox job isn't toggled. Skipping") return next_state - # if job not ready, go next state - if state_history.state_is_ready(state) is False: - logger.log(f"{state} isn't ready. Skipping this state...") - return next_state - return collect_bannerbox_rewards_state(vm_index, logger, next_state) if state == "daily_rewards": # --> battlepass_rewards @@ -513,9 +393,11 @@ def state_tree( logger.log("daily_rewards job isn't toggled. Skipping") return next_state - # if job not ready, go next state - if state_history.state_is_ready(state) is False: - logger.log(f"{state} isn't ready. Skipping this state...") + # if job not ready, return next state + if not logger.check_if_can_collect_daily_rewards( + job_list["daily_reward_increment_user_input"], + ): + logger.log("daily_rewards job isn't ready") return next_state # run this job, return its output @@ -524,16 +406,16 @@ def state_tree( if state == "battlepass_rewards": # --> card_mastery next_state = "card_mastery" - # if job not toggled go next state if not job_list["battlepass_collect_user_toggle"]: - logger.log( + logger.change_status( "Battlepass collect is not toggled. Skipping this state", ) return next_state - # if job not ready, go next state - if state_history.state_is_ready(state) is False: - logger.log(f"{state} isn't ready. Skipping this state...") + if not logger.check_if_can_battlepass_collect( + job_list["battlepass_collect_increment_user_input"], + ): + logger.change_status("Battlepass collect is not ready. Skipping this state") return next_state return collect_battlepass_state(vm_index, logger, next_state) @@ -546,44 +428,33 @@ def state_tree( logger.log("Card mastery job isn't toggled. Skipping this state") return next_state - # if job not ready, go next state - if state_history.state_is_ready(state) is False: - logger.log(f"{state} isn't ready. Skipping this state...") + # if job not ready, reutrn next state + if not logger.check_if_can_collect_card_mastery( + job_list["card_mastery_collect_increment_user_input"], + ): + logger.log("Card mastery job isn't ready. Skipping this state") return next_state # return output of this state return card_mastery_state(vm_index, logger, next_state) - if state == "season_shop": # --> magic_items - next_state = "magic_items" + if state == "season_shop": # --> start_fight + next_state = "start_fight" # if job isnt toggled, return next state if not job_list["season_shop_buys_user_toggle"]: - logger.log("Season shop buys is not toggled. Skipping this state") + logger.change_status("Season shop buys is not toggled. Skipping this state") return next_state - # if job not ready, go next state - if state_history.state_is_ready(state) is False: - logger.log(f"{state} isn't ready. Skipping this state...") + # if job isnt ready yet, return next state + if not logger.check_if_can_buy_season_shop_offers( + job_list["season_shop_buys_increment_user_input"], + ): + logger.change_status("Season shop buys is not ready. Skipping this state") return next_state return collect_season_shop_offers_state(vm_index, logger, next_state) - if state == "magic_items": # --> start_fight - next_state = "start_fight" - - # if job isnt toggled, return next state - if not job_list["magic_items_user_toggle"]: - logger.log("magic items spend is not toggled. Skipping this state") - return next_state - - # if job not ready, go next state - if state_history.state_is_ready(state) is False: - logger.log(f"{state} isn't ready. Skipping this state...") - return next_state - - return spend_magic_items_state(vm_index, logger, next_state) - if state == "start_fight": # --> 1v1_fight, war next_state = "war" @@ -635,11 +506,7 @@ def state_tree( ) return do_2v2_fight_state( - vm_index, - logger, - next_state, - random_fight_mode, - False, + vm_index, logger, next_state, random_fight_mode, False, ) if state == "1v1_fight": # --> end_fight @@ -653,25 +520,17 @@ def state_tree( ) print(f"Fight mode is {mode_used_in_1v1}") return do_1v1_fight_state( - vm_index, - logger, - next_state, - random_fight_mode, - mode_used_in_1v1, - False, + vm_index, logger, next_state, random_fight_mode, mode_used_in_1v1, False, ) if state == "end_fight": # --> war next_state = "war" logger.log( - f"This state: {state} took {str(time.time()- start_time)[:5]} seconds", + f"This state: {state} took {str(time.time() - start_time)[:5]} seconds", ) return end_fight_state( - vm_index, - logger, - next_state, - job_list["disable_win_track_toggle"], + vm_index, logger, next_state, job_list["disable_win_track_toggle"], ) if state == "war": # --> account_switch @@ -682,9 +541,9 @@ def state_tree( logger.log("War job isn't toggled. Skipping this state") return next_state - # if job not ready, go next state - if state_history.state_is_ready(state) is False: - logger.log(f"{state} isn't ready. Skipping this state...") + # if job not ready, reutrn next state + if not logger.check_if_can_do_war(job_list["war_attack_increment_user_input"]): + logger.log("War job isn't ready. Skipping this state") return next_state # return output of this state @@ -707,35 +566,46 @@ def state_tree_tester(vm_index): "card_mastery_user_toggle": False, "free_offer_user_toggle": False, "gold_offer_user_toggle": False, - "trophy_road_1v1_battle_user_toggle": False, - "path_of_legends_1v1_battle_user_toggle": True, - "goblin_queens_journey_1v1_battle_user_toggle": True, - "2v2_battle_user_toggle": False, + "trophy_road_1v1_battle_user_toggle": True, + "path_of_legends_1v1_battle_user_toggle": False, + "goblin_queens_journey_1v1_battle_user_toggle": False, + "2v2_battle_user_toggle": True, "upgrade_user_toggle": False, "war_user_toggle": False, "random_decks_user_toggle": False, "open_bannerbox_user_toggle": False, + "daily_rewards_user_toggle": False, "battlepass_collect_user_toggle": False, "level_up_chest_user_toggle": False, + "upgrade_all_cards_user_toggle": False, "trophy_road_rewards_user_toggle": False, "season_shop_buys_user_toggle": False, - "magic_items_user_toggle": False, # keep these off - "daily_rewards_user_toggle": False, # its just been broken via memu for months (freezes my game when i open the daily rewards) "disable_win_track_toggle": False, "skip_fight_if_full_chests_user_toggle": False, "random_plays_user_toggle": False, "memu_attach_mode_toggle": False, + # job increments + "card_upgrade_increment_user_input": 1, + "shop_buy_increment_user_input": 1, + "request_increment_user_input": 1, + "donate_increment_user_input": 1, + "daily_reward_increment_user_input": 1, + "card_mastery_collect_increment_user_input": 1, + "open_chests_increment_user_input": 1, + "deck_randomization_increment_user_input": 1, + "war_attack_increment_user_input": 1, + "battlepass_collect_increment_user_input": 1, + "level_up_chest_increment_user_input": 1, + "trophy_road_reward_increment_user_input": 1, + "season_shop_buys_increment_user_input": 1, # account switching input info "account_switching_toggle": False, - "account_switch_count": 3, + "account_switching_increment_user_input": 1, + "account_switching_slider": 1, + "next_account": 0, + "random_account_switch_list": [0, 1, 2], } - state_history = StateHistory(logger) - - def clip_that_mat_laptop_pc(): - import pyautogui - - pyautogui.click(856, 943, duration=0.2) while 1: state = state_tree( @@ -743,13 +613,13 @@ def clip_that_mat_laptop_pc(): logger, state, job_list, - state_history, ) if state == "restart": print("Restart state") - # input("Enter to continue...") - clip_that_mat_laptop_pc() + input("Enter to continue...") + # print("Clipping that") + # clip_that() if __name__ == "__main__": - state_tree_tester(1) + state_tree_tester(12) diff --git a/src/pyclashbot/bot/trophy_road_rewards.py b/src/pyclashbot/bot/trophy_road_rewards.py index 1caccd4f6..ccd34a4b9 100644 --- a/src/pyclashbot/bot/trophy_road_rewards.py +++ b/src/pyclashbot/bot/trophy_road_rewards.py @@ -12,13 +12,6 @@ from pyclashbot.memu.client import click, screenshot from pyclashbot.utils.logger import Logger -from pyclashbot.detection.image_rec import ( - find_references, - get_first_location, - make_reference_image_list, - get_file_count, -) - def collect_trophy_road_rewards_state(vm_index: int, logger: Logger, next_state: str): # Verify if already in the clash main menu. @@ -42,18 +35,17 @@ def collect_trophy_road_rewards_state(vm_index: int, logger: Logger, next_state: ) # Return "restart" if still in Path of Legends after clicking. return "restart" - + rewards_collected_result = 0 # Checks if Trophy Road rewards are available and attempts to collect them. while check_for_trophy_road_rewards(vm_index): logger.change_status("Trophy Road rewards detected. Attempting to collect...") - trophy_road_state = collect_trophy_road_rewards( - vm_index, - logger, + trophy_road_state, rewards_collected = collect_trophy_road_rewards( + vm_index, logger, ) - if trophy_road_state is not True: logger.change_status("Failed to collect Trophy Road rewards.") return "restart" + rewards_collected_result += rewards_collected # Check if in Path of Legends mode, attempting to navigate back to Trophy Road if so. if check_if_on_path_of_legends_clash_main(vm_index) is True: logger.change_status( @@ -71,6 +63,12 @@ def collect_trophy_road_rewards_state(vm_index: int, logger: Logger, next_state: # Return "restart" if still in Path of Legends after clicking. return "restart" + if rewards_collected_result == 0: + logger.change_status("No Trophy Road rewards detected.") + else: + logger.change_status( + f"{rewards_collected_result} Trophy Road rewards collected.", + ) return next_state @@ -114,7 +112,7 @@ def check_for_trophy_road_rewards(vm_index): return False -def collect_trophy_road_rewards(vm_index: int, logger: Logger): +def collect_trophy_road_rewards(vm_index, logger): """Attempts to collect Trophy Road rewards by navigating to the Trophy Road rewards menu and claiming available rewards. @@ -128,113 +126,64 @@ def collect_trophy_road_rewards(vm_index: int, logger: Logger): bool: True if rewards collection was successful or no rewards were available, False if an error occurred. """ - # if not on main return False - if not check_if_on_clash_main_menu(vm_index): - logger.change_status( - "Not on main to start collect_trophy_road_rewards()! Returning False" - ) - return False - - - - deadspace_coord = [19,502] - - def get_to_main_after_claim(vm_index): - timeout = 30#s - start_time = time.time() - while time.time() - start_time < timeout: - - - if check_if_on_clash_main_menu(vm_index): - return True - click(vm_index, deadspace_coord[0], deadspace_coord[1]) - time.sleep(1) - return False - - def specific_coord_click_claim_button(vm_index): - #search for the button - coord = find_trophy_road_reward_claim_button(vm_index) - - #if we got a button - if coord is not None: - #collect it - click(vm_index,coord[0],coord[1]) - #handle strikes appearing - time.sleep(1) - if strikes_detected(vm_index): - click(vm_index, 210, 580) - time.sleep(1) - - #back to main for next loop - if get_to_main_after_claim(vm_index) is False: - return False - - return True - - #if we didnt get a button - return False - - - - def pixel_coord_click_claim_button(vm_index): - #search for the button - lrn = find_collect_button(vm_index) #left,right,none - - #if we didnt get a button - if lrn is None: - return False - - #if left - if lrn == "left": - click(vm_index, 170, 337) - #handle strikes appearing + rewards_collected = 0 + # Click to potentially open the Trophy Road rewards menu. + click(vm_index, 210, 250) + time.sleep(2) # Wait for the menu to potentially open. + + # Check if successfully entered the Trophy Road rewards menu. + if check_if_on_trophy_road_rewards_menu(vm_index): + logger.change_status("Successfully entered Trophy Road rewards menu.") + time.sleep(2) + + # Loop until no more collect buttons are found or limit is reached + while find_collect_button(vm_index) != None: + collecting_attempts = 0 + collect_side = find_collect_button(vm_index) + if collect_side == "left": + click(vm_index, 170, 337) + elif collect_side == "right": + click(vm_index, 307, 337) + logger.add_bannerbox_collect() + rewards_collected += 1 time.sleep(1) - if strikes_detected(vm_index): - click(vm_index, 210, 580) + logger.change_status("Collecting rewards...") + # Randomly click left or right to collect rewards et go back to trophy road rewards menu + while ( + not check_if_on_trophy_road_rewards_menu(vm_index) + and collecting_attempts < 30 + ): + if random.randint(0, 1): + click(vm_index, 138, 186) # Left + else: + click(vm_index, 281, 186) # Right + time.sleep(0.5) + click(vm_index, 391, 407) # Deadspace time.sleep(1) - - #else right - else: - click(vm_index, 307, 337) - #handle strikes appearing - time.sleep(1) - if strikes_detected(vm_index): - click(vm_index, 210, 580) - time.sleep(1) - - #back to main - if get_to_main_after_claim(vm_index) is False: - return False - - #good collect loop - return True - - # collect until there are no more rewards - while 1: - logger.change_status('Attempting to collect another reward!') - - # Open the Trophy Road rewards menu. - click(vm_index, 210, 250) - time.sleep(2) # Wait for the menu to potentially open. - if not check_if_on_trophy_road_rewards_menu(vm_index): - logger.change_status("Failed to enter trophy road menu. Returning False") - return False - - - - - if specific_coord_click_claim_button(vm_index) or pixel_coord_click_claim_button(vm_index): - continue - - break - - - - - - - - + collecting_attempts += 1 + if check_if_on_clash_main_menu(vm_index) is True: + # Back to main menu + return True, rewards_collected + if collecting_attempts >= 30: + logger.change_status("Collected too much.") + return False, rewards_collected + if strikes_detected(vm_index) is True: + click(vm_index, 210, 580) + time.sleep(1) + + # Attempt to return to the main menu after collecting or attempting to collect rewards + click(vm_index, 210, 608) + if wait_for_clash_main_menu(vm_index, logger): + logger.change_status( + "Successfully returned to clash main menu after collecting rewards.", + ) + return True, rewards_collected + logger.change_status( + "Failed to return to clash main menu after collecting rewards.", + ) + return False, rewards_collected + logger.change_status("Failed to enter Trophy Road rewards menu.") + return False, rewards_collected def find_collect_button(vm_index): @@ -261,7 +210,6 @@ def find_collect_button(vm_index): # Check the left position for expected colors. left_pixel_color = iar[left_position[1]][left_position[0]] - # print('left_pixel_color',left_pixel_color) if any( pixel_is_equal(left_pixel_color, color, tol=35) for color in expected_colors ): @@ -270,7 +218,6 @@ def find_collect_button(vm_index): # Check the right position for expected colors. right_pixel_color = iar[right_position[1]][right_position[0]] - print("right_pixel_color", right_pixel_color) if any( pixel_is_equal(right_pixel_color, color, tol=35) for color in expected_colors ): @@ -280,7 +227,7 @@ def find_collect_button(vm_index): return None # Return None if no collect button is detected on either side. -def strikes_detected(vm_index) -> bool: +def strikes_detected(vm_index): """Checks if the screen indicates that strikes are detected during the Trophy Road rewards collection process. Args: @@ -318,7 +265,7 @@ def strikes_detected(vm_index) -> bool: return False -def check_if_on_trophy_road_rewards_menu(vm_index) -> bool: +def check_if_on_trophy_road_rewards_menu(vm_index): """Checks if the current screen is the Trophy Road rewards menu based on specific pixel colors. Args: @@ -364,37 +311,5 @@ def check_if_on_trophy_road_rewards_menu(vm_index) -> bool: return True -def find_trophy_road_reward_claim_button(vm_index, delay=2): - def to_wrap(): - image = screenshot(vm_index) - - folder = "trophy_road_reward_claim_button" - - names = make_reference_image_list(get_file_count(folder)) - - locations: list[list[int] | None] = find_references( - image, - folder, - names, - tolerance=0.88, - ) - - coord = get_first_location(locations) - - if coord is None: - return None - - return [coord[1], coord[0]] - - timeout = delay - start_time = time.time() - while time.time() - start_time < timeout: - coord = to_wrap() - if coord is not None: - return coord - - if __name__ == "__main__": - print("\n" * 50) - - c = find_trophy_road_reward_claim_button(1, 2) + pass diff --git a/src/pyclashbot/bot/upgrade_all_cards.py b/src/pyclashbot/bot/upgrade_all_cards.py index 147fbe2d4..807bc5a7a 100644 --- a/src/pyclashbot/bot/upgrade_all_cards.py +++ b/src/pyclashbot/bot/upgrade_all_cards.py @@ -265,6 +265,7 @@ def detect_and_upgrade(vm_index, logger, y_positions): def upgrade_all_cards_state(vm_index, logger: Logger, next_state): logger.change_status(status="Upgrade cards state") + logger.add_card_upgrade_attempt() # If not on clash main, return restart clash_main_check = check_if_on_clash_main_menu(vm_index) diff --git a/src/pyclashbot/bot/upgrade_state.py b/src/pyclashbot/bot/upgrade_state.py index a330540e7..e3878c3d2 100644 --- a/src/pyclashbot/bot/upgrade_state.py +++ b/src/pyclashbot/bot/upgrade_state.py @@ -6,7 +6,6 @@ from pyclashbot.bot.nav import ( check_if_on_clash_main_menu, get_to_card_page_from_clash_main, - check_if_on_card_page, wait_for_clash_main_menu, ) from pyclashbot.detection.image_rec import ( @@ -66,12 +65,18 @@ def upgrade_cards_state(vm_index, logger: Logger, next_state): logger.change_status(status="Upgrade cards state") + logger.add_card_upgrade_attempt() # if not on clash main, return restart print("Making sure on clash main before upgrading cards") clash_main_check = check_if_on_clash_main_menu(vm_index) if clash_main_check is not True: logger.change_status("Not on clash main at the start of upgrade_cards_state()") + # logger.log( + # "These are the pixels the bot saw after failing to find clash main:") + # for pixel in clash_main_check: + # logger.log(f" {pixel}") + return "restart" # get to card page @@ -82,12 +87,25 @@ def upgrade_cards_state(vm_index, logger: Logger, next_state): ) return "restart" - # do card upgrade - if update_cards(vm_index, logger) is False: - logger.change_status("Failed to update cards") - return "restart" + # click a bottom card so it scrolls down the little bit (dogshit clash UI) + print("Clicking bottom card to scroll") + click(vm_index, 163, 403) + + # deadspace click to unclick that card but keep the random scroll + print("Deadspace click") + click(vm_index, 14, 286) + + # upgrade each card + for card_index in range(8): + card_index = 8 - card_index - 1 + + while check_if_card_is_upgradable(vm_index, logger, card_index): + if not upgrade_card(vm_index, logger, card_index): + print("Upgrade card failed, so were done with this card.") + break + print("Upgraded a card") - # get back to main when its done + time.sleep(2) logger.change_status(status="Done upgrading cards") click(vm_index, 211, 607) time.sleep(1) @@ -106,91 +124,6 @@ def upgrade_cards_state(vm_index, logger: Logger, next_state): return next_state -def get_upgradable_cards(vm_index): - def classify_color(color): - # is white? - if color[0] > 200 and color[1] > 200 and color[2] > 200: - return "white" - - # if [ 75 250 33] - if color[0] < 100 and color[2] < 100 and color[1] > 200: - return "green" - - return "else" - - def get_region_pixels(region): - pixels = [] - l, t, w, h = region - - for i, x in enumerate(range(w)): - for j, y in enumerate(range(h)): - - if i % 2 == 0 or j % 2 == 0: - continue - pixels.append(image[t + y][l + x]) - - return pixels - - regions = [ - [46, 256, 64, 11], - [133, 256, 64, 11], - [220, 256, 64, 11], - [307, 256, 64, 11], - [46, 395, 64, 11], - [133, 395, 64, 11], - [220, 395, 64, 11], - [307, 395, 64, 11], - ] - - image = screenshot(vm_index) - - good_indicies = [] - - for i, region in enumerate(regions): - - pixels = get_region_pixels(region) - - colors = [classify_color(pixel) for pixel in pixels] - - color2count = {} - for color in colors: - if color not in color2count: - color2count[color] = 0 - color2count[color] += 1 - - if "green" in color2count and color2count["green"] > 20: - good_indicies.append(i) - - return good_indicies - - -def update_cards(vm_index, logger: Logger) -> bool: - # starts and ends on card page - - # if not on card page, return false - if not check_if_on_card_page(vm_index): - logger.log("Not on card page to start update_cards(). Returning false") - return False - - # click a random card - click(vm_index, 73, 201) - time.sleep(0.3) - - # click deadspace - click(vm_index, 14, 300) - time.sleep(0.3) - - upgradable_indicies = get_upgradable_cards(vm_index) - - for index in upgradable_indicies: - if upgrade_card(vm_index, logger, index) is True: - logger.log("Upgraded a card!") - else: - logger.log("Can't upgraded this card yet") - - return True - - def check_for_second_upgrade_button_condition_1(vm_index) -> bool: """Check if the second upgrade button condition 1 is met. @@ -235,7 +168,7 @@ def check_for_confirm_upgrade_button_condition_1(vm_index) -> bool: return True -def upgrade_card(vm_index, logger: Logger, card_index) -> bool: +def upgrade_card(vm_index, logger: Logger, card_index): """Upgrades a card if it is upgradable. Args: @@ -250,19 +183,18 @@ def upgrade_card(vm_index, logger: Logger, card_index) -> bool: None """ - print("\n") upgraded_a_card = False logger.change_status(status=f"Upgrading card index: {card_index}") # click the card - click(vm_index, CARD_COORDS[card_index][0], CARD_COORDS[card_index][1]) - time.sleep(1) + # click(vm_index, CARD_COORDS[card_index][0], CARD_COORDS[card_index][1]) + # time.sleep(2) # click the upgrade button logger.change_status(status="Clicking the upgrade button for this card") coord = UPGRADE_BUTTON_COORDS[card_index] click(vm_index, coord[0], coord[1]) - time.sleep(1) + time.sleep(2) # click second upgrade button logger.change_status(status="Clicking the second upgrade button") @@ -319,7 +251,7 @@ def upgrade_card(vm_index, logger: Logger, card_index) -> bool: logger.change_status( status="Clicking deadspace after attemping upgrading this card", ) - for _ in range(6): + for _ in range(4): click(vm_index, DEADSPACE_COORD[0], DEADSPACE_COORD[1]) time.sleep(1) @@ -350,21 +282,11 @@ def check_if_pixel_indicates_upgradable_card(pixel) -> bool: def check_for_missing_gold_popup(vm_index): if not check_line_for_color( - vm_index, - x_1=338, - y_1=215, - x_2=361, - y_2=221, - color=(153, 20, 17), + vm_index, x_1=338, y_1=215, x_2=361, y_2=221, color=(153, 20, 17), ): return False if not check_line_for_color( - vm_index, - x_1=124, - y_1=201, - x_2=135, - y_2=212, - color=(255, 255, 255), + vm_index, x_1=124, y_1=201, x_2=135, y_2=212, color=(255, 255, 255), ): return False @@ -377,29 +299,30 @@ def check_for_missing_gold_popup(vm_index): return True -# def check_if_card_is_upgradable(vm_index, logger: Logger, card_index): -# logger.change_status(status=f"Checking if {card_index} is upgradable") +def check_if_card_is_upgradable(vm_index, logger: Logger, card_index): + logger.change_status(status=f"Checking if {card_index} is upgradable") -# # click the selected card -# card_coord = CARD_COORDS[card_index] -# print(f"Clicking the #{card_index} card") -# click(vm_index, card_coord[0], card_coord[1]) -# time.sleep(0.66) + # click the selected card + card_coord = CARD_COORDS[card_index] + print(f"Clicking the #{card_index} card") + click(vm_index, card_coord[0], card_coord[1]) + time.sleep(0.66) -# # see if green uprgade button exists in card context menu -# card_is_upgradable = False -# upgrade_coord = UPGRADE_PIXEL_COORDS[card_index] -# if check_if_pixel_indicates_upgradable_card( -# numpy.asarray(screenshot(vm_index))[upgrade_coord[1]][upgrade_coord[0]], -# ): -# card_is_upgradable = True + # see if green uprgade button exists in card context menu + card_is_upgradable = False + upgrade_coord = UPGRADE_PIXEL_COORDS[card_index] + if check_if_pixel_indicates_upgradable_card( + numpy.asarray(screenshot(vm_index))[upgrade_coord[1]][upgrade_coord[0]], + ): + card_is_upgradable = True -# # deadspace click -# # click(vm_index, 14, 286) + # deadspace click + # click(vm_index, 14, 286) -# print(f"Card #{card_index} is upgradable: {card_is_upgradable}") -# return card_is_upgradable + print(f"Card #{card_index} is upgradable: {card_is_upgradable}") + return card_is_upgradable if __name__ == "__main__": - upgrade_cards_state(1, Logger(None, False), "next_state") + # print(check_which_cards_are_upgradable(12)) + click(12, 209, 466) diff --git a/src/pyclashbot/bot/war_state.py b/src/pyclashbot/bot/war_state.py index e0cc54cb4..ea6804410 100644 --- a/src/pyclashbot/bot/war_state.py +++ b/src/pyclashbot/bot/war_state.py @@ -67,6 +67,7 @@ def find_war_battle_icon(vm_index): def war_state(vm_index: int, logger: Logger, next_state: str): """Method to handle the war state of the bot""" + logger.add_war_attempt() logger.change_status(status="War state") @@ -100,7 +101,7 @@ def war_state(vm_index: int, logger: Logger, next_state: str): logger.change_status(status="Starting a war battle") logger.log("Getting to clan tab") - if get_to_clan_tab_from_clash_main(vm_index, logger) is False: + if get_to_clan_tab_from_clash_main(vm_index, logger) == "restart": logger.log("Error 86868243 Took too long to get to clan tab from clash main") return "restart" @@ -254,7 +255,7 @@ def do_war_battle(vm_index, logger) -> Literal["restart", "good"]: # click a random play coord random_play_coord = (random.randint(63, 205), random.randint(55, 455)) click(vm_index, random_play_coord[0], random_play_coord[1]) - time.sleep(5) + time.sleep(9) logger.change_status(status="Done with this war fight") return "good" @@ -388,7 +389,7 @@ def check_for_locked_clan_war_screen(vm_index): ] for i, p in enumerate(pixels): - + # print(p) if not pixel_is_equal(p, colors[i], tol=10): return False return True diff --git a/src/pyclashbot/bot/worker.py b/src/pyclashbot/bot/worker.py index 0ddb81019..13f156d56 100644 --- a/src/pyclashbot/bot/worker.py +++ b/src/pyclashbot/bot/worker.py @@ -1,7 +1,7 @@ import time -from pyclashbot.bot.states import StateHistory, state_tree -from pyclashbot.memu.launcher import get_vm +from pyclashbot.bot.states import state_tree +from pyclashbot.memu.launcher import check_for_vm from pyclashbot.utils.logger import Logger from pyclashbot.utils.thread import PausableThread, ThreadKilled @@ -13,21 +13,12 @@ def __init__(self, logger: Logger, args, kwargs=None) -> None: self.in_a_clan = False def run(self) -> None: - # parse render mode out of jobs - jobs = self.args # parse thread args - render_mode = "opengl" - if jobs["directx_toggle"] is True: - render_mode = "directx" - try: - #init start state + jobs = self.args # parse thread args + # logger = Logger() state = "start" - #init state manager object - state_history = StateHistory(self.logger) - - #init the vm - vm_index = get_vm(self.logger, render_mode=render_mode) + vm_index = check_for_vm(self.logger) # loop until shutdown flag is set while not self.shutdown_flag.is_set(): @@ -37,8 +28,8 @@ def run(self) -> None: self.logger, state, jobs, - state_history ) + while self.pause_flag.is_set(): time.sleep(0.1) # sleep for 100ms until pause flag is unset diff --git a/src/pyclashbot/detection/image_rec.py b/src/pyclashbot/detection/image_rec.py index 454faffc6..e1e71300c 100644 --- a/src/pyclashbot/detection/image_rec.py +++ b/src/pyclashbot/detection/image_rec.py @@ -81,7 +81,7 @@ def find_and_click_button_by_image(vm_index, folder_name) -> bool: screenshot(vm_index), folder_name, names, - tolerance=0.85, # Adjust the tolerance as needed to improve accuracy + tolerance=0.65, # Adjust the tolerance as needed to improve accuracy ) # Get the first location of the detected reference @@ -349,10 +349,3 @@ def get_line_coordinates(x_1, y_1, x_2, y_2) -> list[tuple[int, int]]: coordinates.append((x_1, y_1)) return coordinates - - -def pixels_match_colors(pixels,colors,tol=10) -> bool: - for i, p in enumerate(pixels): - if not pixel_is_equal(p, colors[i], tol=tol): - return False - return True diff --git a/src/pyclashbot/detection/reference_images/donate_button_icon/18.png b/src/pyclashbot/detection/reference_images/donate_button_icon/18.png deleted file mode 100644 index 04b49198a6493b63e6c74f651aa779df9be4ad78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2402 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 diff --git a/src/pyclashbot/detection/reference_images/donate_button_icon/20.png b/src/pyclashbot/detection/reference_images/donate_button_icon/20.png deleted file mode 100644 index 5d1ac4cdf968cc00e0164e19d3ef9ee5ddcf1fd9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3340 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 diff --git a/src/pyclashbot/detection/reference_images/donate_button_icon/22.png b/src/pyclashbot/detection/reference_images/donate_button_icon/22.png deleted file mode 100644 index a1cd78bb1221c6b3f5d2efef44e4d4e0a83e0b85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2506 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 U!4pp~z5oCK07*qoM6N<$f^%H15C8xG diff --git a/src/pyclashbot/detection/reference_images/donate_button_icon/23.png b/src/pyclashbot/detection/reference_images/donate_button_icon/23.png deleted file mode 100644 index b675fd5fd42eb22d7310f7eb9c2e2929971e1707..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2722 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 LNkvXXu0mjfd5S00001b5ch_0Itp) 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 US(c_Py#N3J07*qoM6N<$f&dI0?*IS* diff --git a/src/pyclashbot/detection/reference_images/donate_button_icon/28.png b/src/pyclashbot/detection/reference_images/donate_button_icon/28.png deleted file mode 100644 index 080e8b02c712b19336801a69f98577c7e833d122..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 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 P00000NkvXXu0mjfn1v#v diff --git a/src/pyclashbot/detection/reference_images/donate_button_icon/31.png b/src/pyclashbot/detection/reference_images/donate_button_icon/31.png deleted file mode 100644 index 8a83f781f4b3dd9046c578ec03e54f222b6c420b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2213 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 QDF6Tf07*qoM6N<$f-&M4$N&HU diff --git a/src/pyclashbot/detection/reference_images/exit_battle_button/10.png b/src/pyclashbot/detection/reference_images/exit_battle_button/10.png new file mode 100644 index 0000000000000000000000000000000000000000..4b40771e4f678f6673109f2d3d01d26c2831a6ad GIT binary patch literal 5039 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 literal 0 HcmV?d00001 diff --git a/src/pyclashbot/detection/reference_images/offers_for_gold/38.png b/src/pyclashbot/detection/reference_images/offers_for_gold/38.png deleted file mode 100644 index ea81ff64a20896e34ebd13c4daed4f588cbbd173..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3691 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 diff --git a/src/pyclashbot/detection/reference_images/offers_for_gold/42.png b/src/pyclashbot/detection/reference_images/offers_for_gold/42.png deleted file mode 100644 index af6c932bbb70d7e34998e6f84cf9547ebc0a7e77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1106 zcmV-Y1g-mtP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1LsLZK~y+Tos(T} z6lEBOule4co$i)hwiH?^r3ECBwqlf$fCoyH5E6~?$i#@h!9T!1F!9I}V?6W-P80$S zqJl(G#4;XJ_ZrXSVggc;=gV=glPZTyx*|^Ne!lYCYwDEoJkPTlVx1 zC`n?L7v{Kj`7HI78s!ocYzx!sAPf}dmpn@4UXC35h|-p7R{CF17zRAPKf#@=-*IfH z$=1-tGFsSOc{=w=kyB>1USjHj%Y{n~j=c9dy9N)Sv>6*f?c>kBI9+>mi}4@7xZ{jvQl7^4%`|PNu>=`c6-)GTTN*TNGC&fx1#ZpCJ8Aw5U|ASMful?{fZ*Qrw z%}S`v`RF-?N|!^N>da0zX+2-3LrJOZK16SyN6riwyLg|S1H)oH*M_Nc(|57fCK;$` zEY(}=?l;-qrPHaz^j1yw9O_|yC1Lvd46WH&9A_J&Z#itQ%yW0*mo_AGs7~ED&(MHL zYUnr}IXWE;%T!3Um{e>Q5|_T$E7VpK7H&>LSf*0(IB{I(&W&?Kk}qkB>MlNDU$0G= zDpboRnm{y5!m~42A~|{3aSKh_I8&P=tlz~jB2liBH97S|hHRfg>Zdgmy7{v++BoE7pFziHNn_JXn3JtMos-iYg zQ6llNES6Uy^f<-$8X`j!MX8EmDI^)y*eEs&*_jA9X@hjJ9>+451p~z_(vmLvOA+pi zgmQHUX_BC>CpLkp)3L3{T0@FNk&zeB=4sgSJw4_5a!g6ApcM%gQ=)mFCw~R>?A*H% zcD_7FZ84y$r@+)yQ-YIXQJ@P%%gL7{!pHbZw|RJTjFp*jG(`q4h-pQZ%$xiUjzjmZVFred(KqngzaRjA Y1Hr$)Ec5n7I{*Lx07*qoM6N<$f>JmMG5`Po diff --git a/src/pyclashbot/detection/reference_images/offers_for_gold/43.png b/src/pyclashbot/detection/reference_images/offers_for_gold/43.png deleted file mode 100644 index 5878b8c972dd27caf22eb5b4fbaa5529ed51bc2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1034 zcmV+l1oiugP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1D{DmK~y+Tt(04D z8$}p~pFP-XuVcqf;?Ts+fi!`(ia4ecNTA#!Ayr&)#by5%KLDf(fhvIn5eQIATPYzS zDOEy_P88dT9oxG;?9L9K?NkxsHeezWo?~(TnDwXMaaKAWkS;m>9!8O3XX`obe|}^sTPLiy z**k54W0M`t&}>?4KB=+u!!F&IEoKW--2XJkt(ymIuCCDXy&k4N%pSN=Kj-{#jeJxj zWko1MF*6e*pD`G*Bj)CZ$<2;(xYy)lZ5MUkq)^E7-rJH-_iqk%w_YWPX~^|x_XX|p zI@337>_nW*kjS;f2+&4|(ecQSX_C`f_7Bde?MMrBoW(_$n?7NG`#CC<_vF_vxmi6W zsY;CJ9Ab{eaK^@pgPoL!Nkdu^U^YG6xJk>fC>=?b-agr3O<_`Ds4{-@qKA?|lBS{C zxMqu(B{5`?n>C0HAsWoK%vg#{vO_8xQEgOdmG|kkYm8(&0`{m?%Ti6Rhq7s6rY0f1 z>Z*{gjz|a~E}*{BJ+LSx`!S)!Cq>u}2x=Z3uS3F-pk{-!@`?1P*F&w*B12w`j6kAv zh^LfD3osf;?qfa4Vn_O9MUqNO6IDWSRG|Y2&}qvPEhS4vst2`AtiA~v%Mn=W73>OX z*ph+Rm`o+nQIc*Y!Y>O{B}k{U@?SJCB^1Xz?O+ZkFp^39ZX|{XG#L?-FC`}TJ^4XI zE7YXKJk@nkr;6I1kLRTr$xjSS>0R7Ro)#i0B{FS_#V)4o$5=wZJvmZ^lv6$rX>~R3 zkWIa=I67*vbD(GjSu(k?D_~*fO@i1P>=zr{n6Ufb>-y6WU-~2#a3PB~RzqrMn#qDm ztjE4;(T-Y}}1s4`SJyd-2WysHu1REH< zK5ygunm^a-yxgv`Q4H`>3oP8Zhhg+la@zb_?VmOm)e6sk`C1NyZ@Is$xK)VL@gpJ? z$tIUCG;VNs7)oZ51k-Fwxz)sNKKSGT^LO4CvwA@I7pUaC30rQbl>h($07*qoM6N<$ Eg5sg!M*si- diff --git a/src/pyclashbot/detection/reference_images/offers_for_gold/44.png b/src/pyclashbot/detection/reference_images/offers_for_gold/44.png deleted file mode 100644 index b26e1af5f60a8f920f7c4e076eff4a219ae77a5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4008 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 KMNUMnLSTZP0Q}?t diff --git a/src/pyclashbot/detection/reference_images/trophy_road_reward_claim_button/3.png b/src/pyclashbot/detection/reference_images/trophy_road_reward_claim_button/3.png deleted file mode 100644 index dbcc66be4b9aee0c6837dcc38f4c9b8875cf88dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1994 zcmV;*2Q~PKP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2VY4er5mK z#tQ~>s6YJvP1q;n^!sfeY^#5vI4szDZhW}ye>=S9FuoVE``9P&pSI`my^`IBL&VYE z^X=P6?&E(#_P@o^BCD1a*rsEIZvffE)C_196>3p|Qk31Mx=nFgqt<}+ZTr6iY}9R~A@+rPA3`IxoVHVo13XjKwhSEU2T!ZKr5s-o~8V0y_ z0UaX|csqQpUEdv!0`}G`(8?+}rfF#* znWMRvvWW6FM=R7=*E-p^ze?QPD+N?{_Fz3V@XpD%F@5F|OvOa*X%-h>y?}Sl{eXM~SXx>_A(MxmZxHzg z?5qJMZ-Xh=C@s<%`8{Yg73JjuDv!(1HWiqe29P&VTiHYYaSq$}HqqltC1ZQjrw8Y1|f&PxVGlL%XHHDw z{PYDdK8|=P@bNwfHjhV%MSS#|kDw^>vz(#n8Z5?iP3}OJWtg&osZ(ch zH3Y=~+P&@Q2)1M4!2&i(gOjga!StnR`2Bt;B&XJ>L(R$PZcn4f-ou^j7ubPgD=36HB*gsfOqFv zR&UxgkxJs#i?3mze*hj{gh7uYQ~Gf-MJwqul-?Zj&&KpT^9h1;k-8>hCBKHdPrk(O zZ~OuCkLKNd&9#76^djA#M*nC(x(3q-41~}KQQ;w?R!b=c$?y&bkht1|$d3{bqD_Ue z`BI())bYyL%lP@#4>9%fS@5KxjGRTOUWQtyT~fn+=hE=YM;u*gN5 z7IiEpCOin8mf#xB#X z#iO|eL=q8*Z64g8o5fdO%s{5-*6ED8T6eW82t2$#ALeHsV!g0I>SVyvoX(k$N%H~~ ziLfM*JYZ#G71^CEVv#7K(HQEKC1z%4@I+e$J1l}Hx%*(wzA66zB=I)By$ zPuD>OV}>t{+8nXx>9#hJb^L`Pe|F|P-99vS$Y%?r0&+Q%{sl~pH0R@K4GWW8f(`ND zyd2CLxrRcem0U>lij+lIs8t1lDTyMFL4B?QqoRYgIS8Ua6^Zufls%F>_?Q4=i+r}| z{J>BprE@zj#}IwL3qt3?zX9{+Cs%Bq1UkZ9P{%l>%}XhWqR_1B)Er+TFQp`4H!Qa( zFg!yuw46#I>me7?DN|6HwCIn?{gZY;7xeZ8+_Urok&&J6lugLpD4N_QoJ5KupEVpg z^xX|vI@{Cjca_D8EnWoX1N+0~t|Nd0+D8EYC;Ossq{uQqZd^Sw0me+%e{yulURHm4 cM9A9sH)QUTN}Z&3QUCw|07*qoM6N<$f)I?OPXGV_ diff --git a/src/pyclashbot/detection/reference_images/trophy_road_reward_claim_button/4.png b/src/pyclashbot/detection/reference_images/trophy_road_reward_claim_button/4.png deleted file mode 100644 index 844d966b295a35d93a71093f9bbf46d7c360e294..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1815 zcmV+y2k7{TP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2CPX$K~z{r?UvbZ z8`l-af3tD8i`qzvq~t|%BwKb;+i7FN30x;GY}alIq-}uqDSheR&__S?U+7EwR-{3n zino3A^O_XE5cdnXO5AWeeXW!uT3$Nf94)cq1)GY&9lTmaP zDIohA>4;%CCi0jzg1(@T|L`%c;gU<{*pnEdC*DJ%H$kpH>#w_`35?|`(t^WYXON?b z=Q))-!I}N1IXiNO(Z~U|@06JO-Cb(ex5(6zj0Z<~vG*t^)8Ao)eOPxJ=vx}kk00Ua z=(iY4jdFNwoZ;Ls%-gI^EVI5|VB6jnvqD+?mYoXfPrmyGQ<(?+_Se6mVH!_Msnr(Y zk!&QTbHg+QD~8R>FTTR-=Uzt-=~S9kqJap#pq{&V*a?&F7{AMnwgfAQhUb@G)0QE|pA6pzieYawUj zSzbH#D#HWANUr1`^kCVRAlpUSxz!ehmCmL)cJL^NQe(sdF&38>DV0jZv?$MI5746} z7#tep6y-Kdh{KUbDTh-Go@<+0VK;WYga zo1$PEt{_Sb*=(D1X>qzjaTG-n^5CDvaYRPAo|zrkLMGCg0s8v;1lcmSBRmw<&Q?FY9p?S_ADdTEBY|L`_f ze*Y)(3v2#8@57=qyldfz?TU&Juq@k`8CMwQ!86v=#3%aYXgNIVA?u3n$K5o9l+;Du?TED`;%wVOXa%EB*1m5$)Oyatfy=5#l>Qg{8pZtT_>c57>K6Hc4f$RXIPzHWo>nh zvQcKUQWW~uBHNSU()mmL?9xy9-tn_!2Qny8p`)7UHX>a-co7SvbfU)+gbzefBAs0Q zB9t?4zIOTJyEnLg?VprOWk$vipv42+n!L^ZJNM}y=p)drbL-A6{{7Jeby0b3txdgQ zh!@#t0ac_xh_Cph0TKB84eykL~1C_(!(W|XN2$`8D?PL080-RdA#r$ z@pOVzCPgeBBi<>o&oSdsI1k?C%CkxVCvb;XEwMbU#nN}DBSHXc!|mq_XT z3}=Q31T-GqouNA3p!eq)^z5_St&}^r*Hz;xfn11IzJXJ;{Lp%#_S%>h)R097uvXUX?nnS!h4=PprR%NSSwB3 zlE`wmlou)|sPX2p;6Wkm2ooJU@*h2VS;iMPlCwtU{CUR03ToFzhot*TZt>;AF z9t!znJPXpf?KgMob@$@grtdO*W8~Kb*=u*Zc{;Ake*oP_qZ+Zsm=OQ~002ovPDHLk FV1n|&k!b(` diff --git a/src/pyclashbot/detection/reference_images/trophy_road_reward_claim_button/5.png b/src/pyclashbot/detection/reference_images/trophy_road_reward_claim_button/5.png deleted file mode 100644 index 46360ad01ac69363e1a7b19770396a560412352d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2523 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 diff --git a/src/pyclashbot/detection/reference_images/trophy_road_reward_claim_button/6.png b/src/pyclashbot/detection/reference_images/trophy_road_reward_claim_button/6.png deleted file mode 100644 index 1f4088cffb8dabab7719d69d666bdf51d736ec4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1827 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 P00000NkvXXu0mjfQH*nb diff --git a/src/pyclashbot/interface/controls.py b/src/pyclashbot/interface/controls.py new file mode 100644 index 000000000..8bdb97c8f --- /dev/null +++ b/src/pyclashbot/interface/controls.py @@ -0,0 +1,116 @@ +"""pysimplegui layout for the controls tab""" + +import PySimpleGUI as sg + +from pyclashbot.interface.theme import THEME + +sg.theme(THEME) + + +def make_job_increment_control_object(key): + return sg.InputText( + default_text="1", + tooltip="How often to perform this job", + key=key, + enable_events=True, + size=5, + ) + + +job_increments_titles_column = [ + [sg.Text("Request Random Card Every:")], + [sg.Text("Donate Cards Every:")], + [sg.Text("Collect Free Offer Every:")], + [sg.Text("Upgrade Current Deck Every:")], + [sg.Text("Collect Daily Rewards Every:")], + [sg.Text("Collect Card Mastery Every:")], + [sg.Text("Open Chests Every:")], + [sg.Text("Randomize Deck Every:")], + [sg.Text("Do War Attack Every:")], + [sg.Text("Collect Battlepass Every:")], + [sg.Text("Collect Level Up Chest Every:")], + [sg.Text("Collect Trophy Road Rewards Every:")], + [sg.Text("Collect Season Shop Rewards Every:")], + [sg.Text("Switch Account Every:")], +] + + +games_titles_column = [] + +for _ in range(len(job_increments_titles_column)): + games_titles_column.append([sg.Text("games")]) + + +controls = [ + # whole box + [ + # title column + sg.Column( + job_increments_titles_column, + justification="left", + ), + # input text column + sg.Column( + [ + [make_job_increment_control_object("request_increment_user_input")], + [make_job_increment_control_object("donate_increment_user_input")], + [make_job_increment_control_object("shop_buy_increment_user_input")], + [ + make_job_increment_control_object( + "card_upgrade_increment_user_input", + ), + ], + [ + make_job_increment_control_object( + "daily_reward_increment_user_input", + ), + ], + [ + make_job_increment_control_object( + "card_mastery_collect_increment_user_input", + ), + ], + [make_job_increment_control_object("open_chests_increment_user_input")], + [ + make_job_increment_control_object( + "deck_randomization_increment_user_input", + ), + ], + [make_job_increment_control_object("war_attack_increment_user_input")], + [ + make_job_increment_control_object( + "battlepass_collect_increment_user_input", + ), + ], + [ + make_job_increment_control_object( + "level_up_chest_increment_user_input", + ), + ], + [ + make_job_increment_control_object( + "trophy_road_reward_increment_user_input", + ), + ], + [ + make_job_increment_control_object( + "season_shop_buys_increment_user_input", + ), + ], + [ + make_job_increment_control_object( + "account_switching_increment_user_input", + ), + ], + ], + justification="right", + ), + # end titles column + sg.Column( + games_titles_column, + justification="left", + ), + ], + [sg.VP()], + [sg.HSep(color="lightgray")], +] diff --git a/src/pyclashbot/interface/joblist.py b/src/pyclashbot/interface/joblist.py index e83cb4000..75ad0387f 100644 --- a/src/pyclashbot/interface/joblist.py +++ b/src/pyclashbot/interface/joblist.py @@ -49,21 +49,21 @@ def job_check_box(text: str, element_key: str, default_value=True) -> sg.Checkbo battle_tab = ( [ job_check_box( - "Trophy road battles", + "Trophy road 1v1 battles", "trophy_road_1v1_user_toggle", default_value=False, ), ], [ job_check_box( - "Goblin Queen battles", + "Goblin Queen's Journey battles", "goblin_queens_journey_1v1_battle_user_toggle", default_value=False, ), ], [ job_check_box( - "Path of Legends battles", + "Path of Legends 1v1 battles", "path_of_legends_1v1_user_toggle", default_value=False, ), @@ -82,12 +82,12 @@ def job_check_box(text: str, element_key: str, default_value=True) -> sg.Checkbo ], [ job_check_box( - "Skip win/loss check", "disable_win_track_toggle", default_value=False, + "Disable win/loss tracking", "disable_win_track_toggle", default_value=False, ), ], [ job_check_box( - "Skip fight if full chests", + "Skip fight when full chests", "skip_fight_if_full_chests_user_toggle", default_value=False, ), @@ -101,17 +101,17 @@ def job_check_box(text: str, element_key: str, default_value=True) -> sg.Checkbo ], [ job_check_box( - "Battlepass", "open_battlepass_user_toggle", default_value=False, + "Battlepass rewards", "open_battlepass_user_toggle", default_value=False, ), ], [ job_check_box( - "Card Masteries", "card_mastery_user_toggle", default_value=False, + "Card mastery rewards", "card_mastery_user_toggle", default_value=False, ), ], [ job_check_box( - "Collect Dailies", "daily_rewards_user_toggle", default_value=False, + "Daily Challenge Rewards", "daily_rewards_user_toggle", default_value=False, ), ], [ @@ -121,7 +121,7 @@ def job_check_box(text: str, element_key: str, default_value=True) -> sg.Checkbo ], [ job_check_box( - "Buy Bannerbox Chests", "open_bannerbox_user_toggle", default_value=False, + "Open Bannerbox Chests", "open_bannerbox_user_toggle", default_value=False, ), ], [ @@ -131,13 +131,6 @@ def job_check_box(text: str, element_key: str, default_value=True) -> sg.Checkbo default_value=False, ), ], - [ - job_check_box( - "Spend Magic Items", - "magic_items_user_toggle", - default_value=False, - ), - ], ] @@ -160,7 +153,7 @@ def job_check_box(text: str, element_key: str, default_value=True) -> sg.Checkbo ], [ job_check_box( - "Buy shop offers", + "Buy shop offers for GOLD", "gold_offer_user_toggle", default_value=False, ), @@ -168,11 +161,11 @@ def job_check_box(text: str, element_key: str, default_value=True) -> sg.Checkbo [ job_check_box("Upgrade Cards", "card_upgrade_user_toggle", default_value=False), ], - # [ - # job_check_box( - # "Upgrade ALL Cards", "upgrade_all_cards_user_toggle", default_value=False, - # ), - # ], + [ + job_check_box( + "Upgrade ALL Cards", "upgrade_all_cards_user_toggle", default_value=False, + ), + ], [ job_check_box( "Season Shop Offers", "season_shop_buys_user_toggle", default_value=False, @@ -186,10 +179,10 @@ def job_check_box(text: str, element_key: str, default_value=True) -> sg.Checkbo # layout:List[List[Tab]] sg.TabGroup( layout=[ - [sg.Tab("Battle", battle_tab)], - [sg.Tab("Collection", rewards_tab)], - [sg.Tab("Cards", card_collection_tab)], - ],border_width=0, tab_border_width=0 + [sg.Tab("Battle Jobs", battle_tab)], + [sg.Tab("Collection Jobs", rewards_tab)], + [sg.Tab("Cards Jobs", card_collection_tab)], + ], ), ], ] diff --git a/src/pyclashbot/interface/layout.py b/src/pyclashbot/interface/layout.py index a9e419400..4f15bdf70 100644 --- a/src/pyclashbot/interface/layout.py +++ b/src/pyclashbot/interface/layout.py @@ -6,6 +6,7 @@ import PySimpleGUI as sg from PySimpleGUI import Window +from pyclashbot.interface.controls import controls from pyclashbot.interface.joblist import jobs_checklist from pyclashbot.interface.stats import ( battle_stats, @@ -17,86 +18,70 @@ sg.theme(THEME) -jobs_frame = sg.Frame( - layout=jobs_checklist, - title="Jobs", - expand_x=False, - expand_y=True, - border_width=None, - pad=0, -) -account_switching_switching_frame = sg.Frame( - layout=[ - [ - sg.Checkbox( - "Enabled", - key="account_switching_toggle", - default=False, - ), - ], - [ - sg.Slider( - range=(1, 3), - orientation="h", - key="account_switching_slider", - size=(10, 20), - ), - ], - ], - title="Account Switching", - expand_x=True, - pad=0, -) -memu_settings_frame = sg.Frame( - layout=[ - [ - sg.Checkbox( - "Dock MEmu", - key="memu_attach_mode_toggle", - default=False, - ), - ], - [ - sg.Radio( - enable_events=True, - text="OpenGL", - group_id="render_mode_radio", - default=True, - key="opengl_toggle", - pad=1, - ), - ], - [ - sg.Radio( - enable_events=True, - text="DirectX", - group_id="render_mode_radio", - key="directx_toggle", - pad=1, - ), - ], - ], - title="Memu Settings", - expand_y=True, - expand_x=True, - pad=0, -) - controls_layout = [ [ - sg.Frame(layout=[[jobs_frame]], title="", expand_y=True, border_width=0, pad=0), + sg.Frame(layout=controls, title="Controls", expand_x=True, expand_y=True), + sg.Frame(layout=jobs_checklist, title="Jobs", expand_x=True, expand_y=True), + ], + [ sg.Frame( - layout=[[memu_settings_frame], [account_switching_switching_frame]], - title="", - border_width=0, - pad=0, + layout=[ + [ + sg.Checkbox( + "Enabled", + key="account_switching_toggle", + default=False, + ), + sg.Slider( + range=(1, 8), + orientation="h", + key="account_switching_slider", + size=(10, 20), + ), + ], + [ + sg.Text("Current Account #"), + sg.Text( + "-", + key="current_account", + relief=sg.RELIEF_SUNKEN, + text_color="blue", + size=(5, 1), + ), + ], + [ + sg.Text("Account Order"), + sg.Text( + "-", + key="account_order", + relief=sg.RELIEF_SUNKEN, + text_color="blue", + size=(10, 1), + ), + ], + ], + title="Account Switching", expand_x=True, + ), + sg.Frame( + layout=[ + [ + sg.Checkbox( + "Enabled", + key="memu_attach_mode_toggle", + default=False, + ), + ], + ], + title="Memu Docking", expand_y=True, + expand_x=True, ), - ] + ], ] + stats_tab_layout = [ [ sg.Column( @@ -105,23 +90,20 @@ sg.Frame( layout=battle_stats, title="Battle Stats", - expand_y=False, expand_x=True, - pad=0, + expand_y=True, ), ], [ sg.Frame( layout=bot_stats, title="Bot Stats", - expand_x=False, - expand_y=True, - pad=0, + expand_x=True, ), ], ], + expand_x=True, expand_y=True, - pad=0, ), sg.Column( [ @@ -129,14 +111,14 @@ sg.Frame( layout=collection_stats, title="Collection Stats", + expand_x=True, expand_y=True, - pad=0, ), ], ], + expand_x=True, justification="right", expand_y=True, - pad=0, ), ], ] @@ -163,6 +145,7 @@ main_layout = [ [ + # layout:List[List[Tab]] sg.pin( sg.Column( [ @@ -172,8 +155,6 @@ [sg.Tab("Controls", controls_layout)], [sg.Tab("Stats", stats_tab_layout)], ], - border_width=0, - pad=0, ), ], ], @@ -184,22 +165,25 @@ [ sg.Button( "Start", + expand_x=True, button_color="Lime Green", border_width=3, - size=(10, 1), + # size=(23, 1), ), sg.Button( "Stop", disabled=True, button_color="Red", + expand_x=True, border_width=2, - size=(10, 1), + # size=(23, 1), ), sg.Button( "Collapse", key="-Collapse-Button-", + expand_x=True, border_width=2, - size=(10, 1), + # size=(23, 1), ), ], [time_status_bar_layout], @@ -215,6 +199,7 @@ "donate_toggle", "free_donate_toggle", "card_mastery_user_toggle", + "memu_attach_mode_toggle", "disable_win_track_toggle", "free_offer_user_toggle", "gold_offer_user_toggle", @@ -232,15 +217,25 @@ "trophy_road_rewards_user_toggle", "upgrade_all_cards_user_toggle", "season_shop_buys_user_toggle", - 'magic_items_user_toggle', - + # job increment controls keys + "request_increment_user_input", + "donate_increment_user_input", + "daily_reward_increment_user_input", + "shop_buy_increment_user_input", + "card_upgrade_increment_user_input", + "card_mastery_collect_increment_user_input", + "open_chests_increment_user_input", + "deck_randomization_increment_user_input", + "war_attack_increment_user_input", + "battlepass_collect_increment_user_input", + "account_switching_increment_user_input", + "level_up_chest_increment_user_input", + "level_up_chest_user_toggle", + "trophy_road_reward_increment_user_input", + "season_shop_buys_increment_user_input", # account switching stuff "account_switching_toggle", "account_switching_slider", - # MEmu settings - "memu_attach_mode_toggle", - "opengl_toggle", - "directx_toggle", ] # list of button and checkbox keys to disable when the bot is running @@ -253,22 +248,5 @@ def create_window() -> Window: if not path.isfile(path=icon_path): icon_path = path.join("..\\..\\..\\assets\\", icon_path) return sg.Window( - title=f"py-clash-bot | {__version__}", - layout=main_layout, - icon=icon_path, + title=f"py-clash-bot | {__version__}", layout=main_layout, icon=icon_path, ) - - -def test_window(): - """Method for testing the window layout""" - window = create_window() - while True: - event, values = window.read() - print(event) - if event == sg.WIN_CLOSED: - break - window.close() - - -if __name__ == "__main__": - test_window() diff --git a/src/pyclashbot/interface/stats.py b/src/pyclashbot/interface/stats.py index f6ade4042..4bb605a63 100644 --- a/src/pyclashbot/interface/stats.py +++ b/src/pyclashbot/interface/stats.py @@ -8,7 +8,7 @@ sg.theme(THEME) -def stat_box(stat_name: str, size=(5, 1)) -> sg.Text: +def stat_box(stat_name: str, size=(4, 1)) -> sg.Text: """Returns a pysimplegui text box object for stats layout""" return sg.Text( "0", @@ -16,41 +16,59 @@ def stat_box(stat_name: str, size=(5, 1)) -> sg.Text: relief=sg.RELIEF_SUNKEN, text_color="blue", size=size, - pad=0 ) -def make_stat_titles(titles:list[str])->list[list[sg.Text]]: - list = [[sg.Text(title,pad=0)] for title in titles] - return list - # collection stats -collection_title_texts = [ - "Requests", - "Shop Buys", - "Donates", - "Chests Unlocked", - "Daily Rewards", - "Card Masteries", - "Bannerbox Buys", - "Cards Upgraded", - "Battlepass Collects", - "Level Up Chests", - "War Chests", - "Season Shop Buys", - 'Trophy Road Rewards', - 'Magic Item Buys' -] - -collection_stats_titles: list[list[Text]] = make_stat_titles(collection_title_texts) +collection_stats_titles: list[list[Text]] = [ + [ + sg.Text("Requests: "), + ], + [ + sg.Text("Shop Buys: "), + ], + [ + sg.Text("Donates: "), + ], + [ + sg.Text("Chests Unlocked: "), + ], + [ + sg.Text("Daily Rewards: "), + ], + [ + sg.Text("Card Mastery Rewards: "), + ], + [ + sg.Text("Bannerbox Collects: "), + ], + [ + sg.Text("Cards Upgraded: "), + ], + [ + sg.Text("Shop Offers Collected:"), + ], + [ + sg.Text("Battlepass Reward Collects"), + ], + [ + sg.Text("Level Up Reward Collects"), + ], + [ + sg.Text("War Chest Collects"), + ], + [ + sg.Text("Season Shop Buys"), + ], +] collection_stats_values: list[list[Text]] = [ [ stat_box("requests"), ], [ - stat_box("shop_offer_collections"), + stat_box("shop_buys"), ], [ stat_box("donates"), @@ -70,7 +88,9 @@ def make_stat_titles(titles:list[str])->list[list[sg.Text]]: [ stat_box("upgrades"), ], - + [ + stat_box("shop_offer_collections"), + ], [ stat_box("battlepass_collects"), ], @@ -83,12 +103,6 @@ def make_stat_titles(titles:list[str])->list[list[sg.Text]]: [ stat_box("season_shop_buys"), ], - [ - stat_box("trophy_road_reward_collections"), - ], - [ - stat_box("magic_item_buys"), - ], ] collection_stats = [ @@ -100,21 +114,39 @@ def make_stat_titles(titles:list[str])->list[list[sg.Text]]: # fight stats -titles = [ - 'Wins', - 'Losses', - 'Win Rate', - 'Cards Played', - ' Legends Battles', - 'Trophy Battles', - 'Goblin Fights', - '2v2 Fights', - 'War Fights', - 'Random Decks', -] - -battle_stats_titles: list[list[sg.Text]] =make_stat_titles(titles) +battle_stats_titles: list[list[sg.Text]] = [ + [ + sg.Text("Wins: "), + ], + [ + sg.Text("Losses: "), + ], + [ + sg.Text("Win Rate: "), + ], + [ + sg.Text("Cards Played: "), + ], + [ + sg.Text("Path of Legends Battles: "), + ], + [ + sg.Text("Trophy Road Battles: "), + ], + [ + sg.Text("Goblin Queen Fights: "), + ], + [ + sg.Text("2v2 Fights: "), + ], + [ + sg.Text("War Fights: "), + ], + [ + sg.Text("Random Decks: "), + ], +] battle_stats_values = [ [ @@ -149,34 +181,36 @@ def make_stat_titles(titles:list[str])->list[list[sg.Text]]: battle_stats = [ [ - sg.Column(battle_stats_titles, element_justification="right",pad=0), - sg.Column(battle_stats_values, element_justification="left",pad=0), - + sg.Column(battle_stats_titles, element_justification="right"), + sg.Column(battle_stats_values, element_justification="left"), ], ] # bot stats -bot_stat_title_texts = [ - "Bot Failures", - "Acct Swaps", - "Runtime", +bot_stats_titles: list[list[sg.Text]] = [ + [ + sg.Text("Bot Failures"), + ], + [ + sg.Text("Runtime"), + ], + [ + sg.Text("Account Switches"), + ], ] -bot_stats_titles: list[list[sg.Text]] = make_stat_titles(bot_stat_title_texts) - bot_stats_values = [ [ stat_box("restarts_after_failure"), ], [ - stat_box("account_switches", size=(7, 1)), + stat_box("time_since_start", size=(7, 1)), ], [ - stat_box("time_since_start", size=(7, 1)), + stat_box("account_switches", size=(7, 1)), ], - ] bot_stats = [ diff --git a/src/pyclashbot/memu/client.py b/src/pyclashbot/memu/client.py index 280323d0f..af15cff42 100644 --- a/src/pyclashbot/memu/client.py +++ b/src/pyclashbot/memu/client.py @@ -62,8 +62,8 @@ def scroll_up_fast(vm_index): send_swipe(vm_index, 215, 100, 215, 500) -def custom_swipe(vm_index, start_x, start_y, end_x, end_y, count=1, delay=0): - for _ in range(count): +def custom_swipe(vm_index, start_x, start_y, end_x, end_y, repeat, delay): + for _ in range(repeat): send_swipe(vm_index, start_x, start_y, end_x, end_y) time.sleep(delay) @@ -92,10 +92,8 @@ def scroll_down(vm_index): def scroll_down_in_request_page(vm_index): """Method for scrolling down faster when interacting with a scrollable menu""" - # vertical swipe - send_swipe(vm_index, 43, 350, 43, 140) + send_swipe(vm_index, 43, 350, 43, 280) - # horizontal swipe to stop the scroll send_swipe(vm_index, 100, 385, 330, 385) @@ -110,29 +108,14 @@ def scroll_down_slowly_in_shop_page(vm_index): """Method for scrolling down even faster when interacting with a scrollable menu using the left side of the screen """ - send_swipe(vm_index, 66, 400, 66, 200) - - # click deadspace to stop the scroll - click(vm_index, 10, 200) - - -def scroll_up_in_shop_page(vm_index): - """Method for scrolling down even faster when interacting with a - scrollable menu using the left side of the screen - """ - for _ in range(4): - send_swipe(vm_index, 66, 60, 66, 600) + send_swipe(vm_index, 66, 400, 66, 350) # click deadspace to stop the scroll click(vm_index, 10, 200) def send_swipe( - vm_index: int, - x_coord1: int, - y_coord1: int, - x_coord2: int, - y_coord2: int, + vm_index: int, x_coord1: int, y_coord1: int, x_coord2: int, y_coord2: int, ): """Method for sending a swipe command to the given vm @@ -229,5 +212,4 @@ def send_newline_char(vm_index): if __name__ == "__main__": - for i in range(4): - scroll_up_in_shop_page(1) + pass diff --git a/src/pyclashbot/memu/configure.py b/src/pyclashbot/memu/configure.py index ef32dea66..e4d6e67eb 100644 --- a/src/pyclashbot/memu/configure.py +++ b/src/pyclashbot/memu/configure.py @@ -16,7 +16,7 @@ "win_scaling_percent2": 100, # 100% scaling "is_customed_resolution": 1, "resolution_width": 419, - "graphics_render_mode": 0, # opengl + "graphics_render_mode": "1", # use DirectX to avoid black screenshot issue "resolution_height": 633, "vbox_dpi": 160, "cpucap": 50, @@ -42,32 +42,15 @@ def set_vm_language(vm_index: int): time.sleep(0.33) -def configure_vm(vm_index, render_mode): - # render_mode = either 'opengl' or 'directx' - print('Configuring vm with render mode:', render_mode) - - # render mode type safety - try: - render_mode = str(render_mode).lower() - if render_mode not in ["opengl", "directx"]: - print('Render mode must be either "opengl" or "directx"\nRecieved:', render_mode) - raise ValueError - except: - return False - - # set render mode according to the param - MEMU_CONFIGURATION["graphics_render_mode"] = 1 # directx - if render_mode == "opengl": - MEMU_CONFIGURATION["graphics_render_mode"] = 0 # opengl - - # do config for each key - print('Setting each config key...') +def configure_vm(vm_index): + """Configure the virtual machine with the given index.""" logging.info("Configuring VM %s...", vm_index) + for key, value in MEMU_CONFIGURATION.items(): pmc.set_configuration_vm(key, str(value), vm_index=vm_index) - # set language - print('Setting vm language...') + set_vm_language(vm_index=vm_index) + set_vm_language(vm_index=vm_index) set_vm_language(vm_index=vm_index) logging.info("Configured VM %s", vm_index) diff --git a/src/pyclashbot/memu/launcher.py b/src/pyclashbot/memu/launcher.py index e16cb9d64..ebd417ad3 100644 --- a/src/pyclashbot/memu/launcher.py +++ b/src/pyclashbot/memu/launcher.py @@ -34,6 +34,8 @@ def check_vm_size(vm_index): home_button_press(vm_index, clicks=1) image = screenshot(vm_index) + print(image.size) + print(image.shape) height, width, _ = image.shape @@ -41,6 +43,7 @@ def check_vm_size(vm_index): print(f"Size is bad: {width},{height}") return False + print(f"Size is good: {width},{height}") return True except Exception as e: print("sizing error:", e) @@ -49,7 +52,7 @@ def check_vm_size(vm_index): return True -def restart_emulator(logger, render_mode, start_time=time.time(), open_clash=True): +def restart_emulator(logger, start_time=time.time(), open_clash=True): """Restart the emulator. Args: @@ -63,7 +66,9 @@ def restart_emulator(logger, render_mode, start_time=time.time(), open_clash=Tru close_everything_memu() # check for the pyclashbot vm, if not found then create it - vm_index = get_vm(logger, render_mode) + vm_index = check_for_vm(logger) + + configure_vm(vm_index=vm_index) # start the vm logger.change_status(status="Opening the pyclashbot emulator...") @@ -73,12 +78,12 @@ def restart_emulator(logger, render_mode, start_time=time.time(), open_clash=Tru # skip ads if skip_ads(vm_index) == "fail": logger.log("Error 99 Failed to skip ads") - return restart_emulator(logger, render_mode, start_time) + return restart_emulator(logger, start_time) print(check_vm_size(vm_index)) if not check_vm_size(vm_index): logger.log("Error 1010 VM size is bad") - return restart_emulator(logger, render_mode, start_time) + return restart_emulator(logger, start_time) # if open_clash is toggled, open CR if open_clash: @@ -104,7 +109,7 @@ def restart_emulator(logger, render_mode, start_time=time.time(), open_clash=Tru return True # Successfully handled starting battle or end-of-battle scenario if battle_start_result == "restart": # Need to restart the process due to issues detected - return restart_emulator(logger, render_mode, start_time) + return restart_emulator(logger, start_time) # click deadspace click(vm_index, 5, 350) @@ -118,7 +123,7 @@ def restart_emulator(logger, render_mode, start_time=time.time(), open_clash=Tru logger.log("Clash main wait timed out! These are the pixels it saw:") # for p in clash_main_check: # logger.log(p) - return restart_emulator(logger, render_mode, start_time) + return restart_emulator(logger, start_time) print("Skipping clash open sequence") logger.log(f"Took {str(time.time() - start_time)[:5]}s to launch emulator") @@ -147,30 +152,50 @@ def skip_ads(vm_index): return "success" -def get_vm(logger: Logger, render_mode) -> int: - # find existing vm - find_vm_timeout = 5 # s +def check_for_vm(logger: Logger) -> int: + """Check for a vm named pyclashbot, create one if it doesn't exist + + Args: + ---- + logger (Logger): Logger object + + Returns: + ------- + int: index of the vm + + """ + start_time = time.time() + + find_vm_timeout = 10 # s find_vm_start_time = time.time() - vm_index = -1 - while vm_index == -1 and time.time() - find_vm_start_time < find_vm_timeout: + find_vm_tries = 0 + while time.time() - find_vm_start_time < find_vm_timeout: + find_vm_tries += 1 vm_index = get_vm_index(EMULATOR_NAME) - if vm_index != -1: - logger.change_status(f'Found a vm named "pyclashbot" (#{vm_index})!') - - # if we didnt find a vm, make a new one - if vm_index == -1: - logger.change_status('Failed to find an existing VM. Creating a new one...') - print("Creating a new vm...") - vm_index = create_vm() - print("Renaming this new vm to", EMULATOR_NAME) - rename_vm(vm_index, name=EMULATOR_NAME) - - # config the vm - print("Configuring the vm...") - configure_vm(vm_index, render_mode) - - print("Done in get_vm()") - return vm_index + + if vm_index != -1: + logger.change_status( + f'Found a vm named "pyclashbot" (#{vm_index}) in {find_vm_tries} tries', + ) + return vm_index + + logger.change_status('Failed to find "pyclashbot" emulator. Retrying...') + + logger.change_status("Didn't find a vm named 'pyclashbot', creating one...") + + new_vm_index = create_vm() + logger.change_status(f"New VM index is {new_vm_index}") + + configure_vm(vm_index=new_vm_index) + + logger.change_status("Setting language") + rename_vm(vm_index=new_vm_index, name=EMULATOR_NAME) + + logger.change_status( + f"Created and configured new pyclashbot emulator in {str(time.time() - start_time)[:5]}s", + ) + + return new_vm_index def start_clash_royale(logger: Logger, vm_index): @@ -203,12 +228,10 @@ def start_clash_royale(logger: Logger, vm_index): # making/configuring emulator methods -def create_vm() -> int: +def create_vm(): """Create a vm with the given name and version""" - print("Starting console") start_memuc_console() - print("Creating vm...") vm_index = pmc.create_vm(vm_version="96") return vm_index @@ -283,7 +306,8 @@ def close_clash_royale_app(logger, vm_index): def close_everything_memu(): - """Closes all MEmu processes.""" + """Closes all MEmu processes. + """ name_list = [ "MEmuConsole.exe", "MEmu.exe", @@ -304,7 +328,8 @@ def close_everything_memu(): def show_clash_royale_setup_gui(): - """Displays a GUI window indicating that Clash Royale is not installed or setup.""" + """Displays a GUI window indicating that Clash Royale is not installed or setup. + """ out_text = """Clash Royale is not installed or setup. Please install Clash Royale, finish the in-game tutorial and login before using this bot.""" @@ -368,5 +393,42 @@ def check_for_emulator_running(vm_index): return False +def reset_clashbot_emulator(logger): + # find which index emulator is at with a 60s timeout + vm_index = get_clashbot_vm_index() + + logger.change_status("Failed to find an existing clashbot emulator. Creating one!") + + # delete old emulator + if vm_index is not False: + logger.change_status("Deleting old emulator...") + if delete_vm(vm_index): + logger.change_status( + f"Successfully deleted old emulator of index {vm_index}!", + ) + else: + logger.change_status(f"Failed to delete old emulator of index {vm_index}") + + # make new emulator + vm_index = create_vm() + + # boot the emulator + while 1: + # close the emulator + stop_vm(vm_index) + time.sleep(5) + + # configure emulator + configure_vm(vm_index) + + # boot up emulator, if successful, break + if restart_emulator(logger, start_time=time.time(), open_clash=False) is True: + break + + logger.change_status( + "Emualtor refreshed! Install Clash Royale and restart the bot.", + ) + + if __name__ == "__main__": pass diff --git a/src/pyclashbot/memu/memu_closer.py b/src/pyclashbot/memu/memu_closer.py index a166d042c..eba959a9f 100644 --- a/src/pyclashbot/memu/memu_closer.py +++ b/src/pyclashbot/memu/memu_closer.py @@ -29,7 +29,7 @@ def terminate_process_by_pid(pid): return False -def close_everything_memu(): +def close_memuc_processes(): processes = list_running_processes() bad_pid_list = [] diff --git a/src/pyclashbot/utils/logger.py b/src/pyclashbot/utils/logger.py index ad6175827..2a2e86d9d 100644 --- a/src/pyclashbot/utils/logger.py +++ b/src/pyclashbot/utils/logger.py @@ -34,8 +34,7 @@ def compress_logs() -> None: ) if len(logs) >= LOGS_TO_KEEP: with zipfile.ZipFile( - archive_name, - "a" if exists(archive_name) else "w", + archive_name, "a" if exists(archive_name) else "w", ) as archive: for log in logs[:-LOGS_TO_KEEP]: archive.write(log, log.split("\\")[-1]) @@ -117,25 +116,37 @@ def __init__( # job stats self.battlepass_collects = 0 + self.battlepass_collect_attempts = 0 self.bannerbox_collects = 0 self.cards_upgraded = 0 + self.card_upgrade_attempts = 0 self.card_mastery_reward_collections = 0 + self.chest_unlock_attempts = 0 + self.card_mastery_reward_collection_attempts = 0 self.chests_unlocked = 0 self.daily_rewards = 0 self.donates = 0 + self.daily_reward_attempts = 0 + self.donate_attempts = 0 + self.deck_randomize_attempts = 0 + self.request_attempts = 0 self.requests = 0 self.shop_offer_collections = 0 + self.shop_buy_attempts = 0 + self.war_attempts = 0 self.war_chest_collects = 0 self.level_up_chest_collects = 0 - self.trophy_road_reward_collections = 0 + self.level_up_chest_attempts = 0 + self.trophy_road_reward_collect_attempts = 0 self.season_shop_buys = 0 - self.magic_item_buys = 0 + self.season_shop_buys_attempts = 0 # account stuff - self.current_account = -1 + self.account_order = "-" + self.current_account = "-" self.account_switches = 0 - self.account_index_history = [] - self.account2clan = {} + self.switch_account_attempts = 0 + self.in_a_clan = False # restart stats self.auto_restarts = 0 @@ -145,8 +156,10 @@ def __init__( # bot stats self.current_state = "No state" self.current_status = "Idle" + self.time_of_last_request = 0 self.time_of_last_card_upgrade = 0 self.time_of_last_free_offer_collection = 0 + self.total_accounts = 0 # track errored logger self.errored = False @@ -182,8 +195,6 @@ def _update_stats(self) -> None: "bannerbox_collects": self.bannerbox_collects, "daily_rewards": self.daily_rewards, "season_shop_buys": self.season_shop_buys, - "trophy_road_reward_collections": self.trophy_road_reward_collections, - "magic_item_buys": self.magic_item_buys, # card stats "upgrades": self.cards_upgraded, "requests": self.requests, @@ -193,7 +204,9 @@ def _update_stats(self) -> None: "restarts_after_failure": self.restarts_after_failure, "current_status": self.current_status, # account stuff + "current_account": self.current_account, "account_switches": self.account_switches, + "account_order": self.account_order, } def get_stats(self): @@ -205,7 +218,7 @@ def get_stats(self): return stats @staticmethod - def _updates_gui(func): + def _updates_log(func): """Decorator to specify functions which update the queue with statistics""" @wraps(func) @@ -234,59 +247,12 @@ def calc_time_since_start(self) -> str: hours, minutes, seconds = 0, 0, 0 return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}" - def add_account_to_account_history(self, i): - self.account_index_history.append(i) - - def get_next_account(self, total_count): - def get_lowest_value_in_dict(search_dict) -> int: - lowest_val = None - lowest_index = None - - for index, val in search_dict.items(): - if (lowest_val is None) or (val < lowest_val): - lowest_val = val - lowest_index = index - - if lowest_index is None: - return 0 - - # get a list of all the keys that have that value - lowest_keys = [] - for key, value in search_dict.items(): - if int(value) == int(search_dict[lowest_index]): - lowest_keys.append(key) - random_lowest_key = random.choice(lowest_keys) - - return int(random_lowest_key) - - def print_account_history_dict(index2count): - print(f"\nAccount history dict:") - print("{:^6} : {}".format("Acct #", "count")) - for index, count in index2count.items(): - print("{:^6} : {}".format(index, count)) - print("\n") - - # make a dict that represents index2count for each index in range(total_count) - index2count = {i: 0 for i in range(total_count)} - - # populate the dict with the counts of each index in account_index_history - for i in self.account_index_history: - index2count[i] += 1 - - # print the dict - print_account_history_dict(index2count) - - # get the index with the lowest count - next_account = get_lowest_value_in_dict(index2count) - - return next_account + def increment_season_shop_buys_attempts(self): + self.season_shop_buys_attempts += 1 def increment_season_shop_buys(self): self.season_shop_buys += 1 - def increment_magic_item_buys(self): - self.magic_item_buys += 1 - def increment_queens_journey_fights(self): self.queens_journey_fights += 1 @@ -296,9 +262,6 @@ def increment_trophy_road_fights(self): def increment_path_of_legends_fights(self): self.path_of_legends_1v1_fights += 1 - def increment_trophy_road_reward_collects(self): - self.trophy_road_reward_collections += 1 - def pick_lowest_fight_type_count(self, mode2toggle): _2v2_fights = self._2v2_fights trophy_road_1v1_fights = self.trophy_road_1v1_fights @@ -327,17 +290,10 @@ def pick_lowest_fight_type_count(self, mode2toggle): lowest_fight_type = min(mode2count, key=mode2count.get) return lowest_fight_type - def is_in_clan(self): - if self.current_account in self.account2clan: - return self.account2clan[self.current_account] - return False - def update_in_a_clan_value(self, in_a_clan: bool): - self.account2clan[self.current_account] = in_a_clan + self.in_a_clan = in_a_clan - # frontend stats - - @_updates_gui + @_updates_log def calc_win_rate(self): """Calculate win rate using logger's stats and return as string""" wins = self.wins @@ -355,22 +311,22 @@ def calc_win_rate(self): ratio = str(100 * (wins / (wins + losses)))[:4] + "%" return ratio - @_updates_gui + @_updates_log def set_current_state(self, state_to_set): """Set logger's current_state to state_to_set""" self.current_state = state_to_set - @_updates_gui + @_updates_log def increment_battlepass_collects(self): """Increment the logger's battlepass_collects count by 1.""" self.battlepass_collects += 1 - @_updates_gui + @_updates_log def add_shop_offer_collection(self) -> None: """Add level up chest collection to log""" self.shop_offer_collections += 1 - @_updates_gui + @_updates_log def error(self, message: str) -> None: """Log error message @@ -382,88 +338,92 @@ def error(self, message: str) -> None: self.errored = True logging.error(message) - @_updates_gui + @_updates_log def add_card_mastery_reward_collection(self) -> None: """Increment logger's card mastery reward collection counter by 1""" self.card_mastery_reward_collections += 1 - @_updates_gui + @_updates_log def add_chest_unlocked(self) -> None: """Add chest unlocked to log""" self.chests_unlocked += 1 - @_updates_gui + @_updates_log def add_war_fight(self) -> None: """Add card played to log""" self.war_fights += 1 - @_updates_gui + @_updates_log def add_card_played(self) -> None: """Add card played to log""" self.cards_played += 1 - @_updates_gui + @_updates_log def add_level_up_chest_collect(self): self.level_up_chest_collects += 1 - @_updates_gui + @_updates_log + def add_level_up_chest_attempt(self): + self.level_up_chest_attempts += 1 + + @_updates_log def add_card_upgraded(self): """Add card upgraded to log""" self.cards_upgraded += 1 - @_updates_gui + @_updates_log def add_win(self): """Add win to log""" self.wins += 1 self.winrate = self.calc_win_rate() - @_updates_gui + @_updates_log def add_loss(self): """Add loss to log""" self.losses += 1 self.winrate = self.calc_win_rate() - @_updates_gui + @_updates_log def add_1v1_fight(self) -> None: """Add fight to log""" self._1v1_fights += 1 - @_updates_gui + @_updates_log def add_card_randomization(self): """Incremenet card_randomizations counter""" self.card_randomizations += 1 - @_updates_gui + @_updates_log def increment_account_switches(self): """Incremenet account_switches counter""" self.account_switches += 1 - @_updates_gui + @_updates_log def increment_2v2_fights(self) -> None: """Add fight to log""" self._2v2_fights += 1 - @_updates_gui + @_updates_log def add_request(self) -> None: """Add request to log""" self.requests += 1 - @_updates_gui + @_updates_log def add_war_chest_collect(self) -> None: """Add request to log""" self.war_chest_collects += 1 - @_updates_gui + @_updates_log def add_donate(self) -> None: """Add donate to log""" self.donates += 1 - @_updates_gui + @_updates_log def add_daily_reward(self) -> None: """Add donate to log""" self.daily_rewards += 1 - @_updates_gui + @_updates_log def change_status(self, status) -> None: """Change status of bot in log @@ -475,15 +435,57 @@ def change_status(self, status) -> None: self.current_status = status self.log(status) - @_updates_gui + @_updates_log def add_restart_after_failure(self) -> None: """Add request to log""" self.restarts_after_failure += 1 + @_updates_log + def change_current_account(self, account_id): + self.current_account = account_id + + @_updates_log + def update_account_order_var(self, account_order): + self.account_order = account_order + + def add_randomize_deck_attempt(self): + """Increments logger's deck_randomize_attempts by 1""" + self.deck_randomize_attempts += 1 + + def add_request_attempt(self): + """Increments logger's request_attempts by 1""" + self.request_attempts += 1 + + def add_donate_attempt(self): + self.donate_attempts += 1 + + def add_card_mastery_reward_collection_attempts(self): + self.card_mastery_reward_collection_attempts += 1 + + def add_shop_buy_attempt(self): + """Increments logger's free_offer_collection_attempts by 1""" + self.shop_buy_attempts += 1 + def add_bannerbox_collect(self): """Increments logger's bannerbox_collects by 1""" self.bannerbox_collects += 1 + def add_card_upgrade_attempt(self): + """Increments logger's card_upgrade_attempts by 1""" + self.card_upgrade_attempts += 1 + + def add_chest_unlock_attempt(self): + """Increments logger's chest_unlock_attempts by 1""" + self.chest_unlock_attempts += 1 + + def add_war_attempt(self): + """Increments logger's war_attempts by 1""" + self.war_attempts += 1 + + def add_switch_account_attempt(self) -> None: + """Add card played to log""" + self.switch_account_attempts += 1 + def get_1v1_fights(self) -> int: """Returns logger's 1v1_fights stat""" return self._1v1_fights @@ -508,6 +510,548 @@ def get_chests_opened(self): """Return chests_unlocked stat""" return self.chests_unlocked + def add_trophy_reward_collect_attempt(self): + self.trophy_road_reward_collect_attempts += 1 + + def check_if_can_collect_trophy_road_rewards(self, increment): + increment = int(increment) + if increment <= 1: + self.log( + f"Increment is {increment} so can always collect_trophy_road_rewards", + ) + return True + + # count trophy_road_reward_collect_attempts + trophy_road_reward_collect_attempts = self.trophy_road_reward_collect_attempts + + # count games + games_played = self._1v1_fights + self._2v2_fights + self.war_fights + + # if trophy_road_reward_collect_attempts is zero return true + if trophy_road_reward_collect_attempts == 0: + self.log( + f"Can collect_trophy_road_rewards. {games_played} Games and {trophy_road_reward_collect_attempts} Attempts", + ) + return True + + # if games_played is zero return true + if games_played == 0: + self.log( + f"Can collect_trophy_road_rewards. {games_played} Games and {trophy_road_reward_collect_attempts} Attempts", + ) + return True + + # if games_played / increment > trophy_road_reward_collect_attempts + if games_played / increment >= trophy_road_reward_collect_attempts: + self.log( + f"Can collect_trophy_road_rewards. {games_played} Games and {trophy_road_reward_collect_attempts} Attempts", + ) + return True + + self.log( + f"Can't collect_trophy_road_rewards. {games_played} Games and {trophy_road_reward_collect_attempts} Attempts", + ) + return False + + def check_if_can_open_chests(self, increment): + """Check if can open chests using logger's games_played and + open_chests attempts stats and user input increment arg + """ + increment = int(increment) + if increment <= 1: + self.log(f"Increment is {increment} so can always open chests") + return True + + # count chest_unlock_attempts + chest_unlock_attempts = self.chest_unlock_attempts + + # count games + games_played = self._1v1_fights + self._2v2_fights + self.war_fights + + # if chest_unlock_attempts is zero return true + if chest_unlock_attempts == 0: + self.log( + f"Can open chests. {games_played} Games and {chest_unlock_attempts} Attempts", + ) + return True + + # if games_played is zero return true + if games_played == 0: + self.log( + f"Can open chests. {games_played} Games and {chest_unlock_attempts} Attempts", + ) + return True + + # if games_played / increment > chest_unlock_attempts + if games_played / increment >= chest_unlock_attempts: + self.log( + f"Can open chests. {games_played} Games and {chest_unlock_attempts} Attempts", + ) + return True + + self.log( + f"Can't open chests. {games_played} Games and {chest_unlock_attempts} Attempts", + ) + return False + + def check_if_can_collect_card_mastery(self, increment) -> bool: + """Check if can collect card mastery rewards using logger's games_played and + card_mastery_reward_collection_attempts stats and user input increment arg + """ + increment = int(increment) + if increment <= 1: + self.log(f"Increment is {increment} so can always collect card mastery") + return True + + # count card_mastery_reward_collection_attempts + card_mastery_attempts = self.card_mastery_reward_collection_attempts + + # count games + games_played = self._1v1_fights + self._2v2_fights + self.war_fights + + # if card_mastery_reward_collection_attempts is zero return true + if card_mastery_attempts == 0: + self.log( + f"Can do card mastery. {games_played} Games and {card_mastery_attempts} Attempts", + ) + return True + + # if games_played is zero return true + if games_played == 0: + self.log( + f"Can do card mastery. {games_played} Games and {card_mastery_attempts} Attempts", + ) + return True + + # if games_played / increment > card_mastery_reward_collection_attempts + if games_played / increment >= card_mastery_attempts: + self.log( + f"Can do card mastery. {games_played} Games & {card_mastery_attempts} Attempts", + ) + return True + + self.log( + f"Can't do card mastery. {games_played} Games and {card_mastery_attempts} Attempts", + ) + return False + + def check_if_can_collect_level_up_chest(self, increment) -> bool: + """Check if can collect level up chest rewards using logger's games_played and + level_up_chest_attempts stats and user input increment arg + """ + increment = int(increment) + if increment <= 1: + self.log(f"Increment is {increment} so can always collect level_up_chest") + return True + + # count level_up_chest_attempts + level_up_chest_attempts = self.level_up_chest_attempts + + # count games + games_played = self._1v1_fights + self._2v2_fights + self.war_fights + + # if level_up_chest_attempts is zero return true + if level_up_chest_attempts == 0: + self.log( + f"Can do level_up_chest. {games_played} Games and {level_up_chest_attempts} Attempts", + ) + return True + + # if games_played is zero return true + if games_played == 0: + self.log( + f"Can do level_up_chest. {games_played} Games and {level_up_chest_attempts} Attempts", + ) + return True + + # if games_played / increment > level_up_chest_attempts + if games_played / increment >= level_up_chest_attempts: + self.log( + f"Can do level_up_chest. {games_played} Games & {level_up_chest_attempts} Attempts", + ) + return True + + self.log( + f"Can't do level_up_chest. {games_played} Games and {level_up_chest_attempts} Attempts", + ) + return False + + def check_if_can_do_war(self, increment) -> bool: + increment = int(increment) + if increment <= 1: + self.log(f"Increment is {increment} so can always collect card mastery") + return True + + # count war_attempts + war_attempts = self.war_attempts + + # count games + games_played = self._1v1_fights + self._2v2_fights + self.war_fights + + # if war_attempts is zero return true + if war_attempts == 0: + self.log(f"Can do war. {games_played} Games and {war_attempts} Attempts") + return True + + # if games_played is zero return true + if games_played == 0: + self.log(f"Can do war. {games_played} Games and {war_attempts} Attempts") + return True + + # if games_played / increment > war_attempts + if games_played / increment >= war_attempts: + self.log(f"Can do war. {games_played} Games & {war_attempts} Attempts") + return True + + self.log(f"Can't do war. {games_played} Games and {war_attempts} Attempts") + return False + + def check_if_can_card_upgrade(self, increment): + """Check if can upgrade cards using logger's games_played and + card_upgrade_attempts stats and user input increment arg + """ + print(f"Can upgrade increment is {increment}") + increment = int(increment) + if increment <= 1: + self.log(f"Increment is {increment} so can always upgrade cards") + return True + + # count card_upgrade_attempts + card_upgrade_attempts = self.card_upgrade_attempts + + # count games + games_played = self._1v1_fights + self._2v2_fights + self.war_fights + + # if card_upgrade_attempts is zero return true + if card_upgrade_attempts == 0: + self.log( + f"Can upgrade. {games_played} Games and {card_upgrade_attempts} Attempts", + ) + return True + + # if games_played is zero return true + if games_played == 0: + self.log( + f"Can upgrade. {games_played} Games and {card_upgrade_attempts} Attempts", + ) + return True + + # if games_played / increment > card_upgrade_attempts + if games_played / increment >= card_upgrade_attempts: + self.log("Can upgrade bc games_played / increment > card_upgrade_attempts") + return True + + self.log( + f"Can't upgrade. {games_played} Games and {card_upgrade_attempts} Attempts", + ) + return False + + def check_if_can_request(self, increment) -> bool: + """Method to check if can request given attempts, games played, and user increment input""" + increment = int(increment) + if increment <= 1: + self.log(f"Increment is {increment} so can always Request") + return True + + # count requests + request_attempts = self.request_attempts + + # count games + games_played = self._1v1_fights + self._2v2_fights + self.war_fights + + # if request_attempts is zero return true + if request_attempts == 0: + self.log( + f"Can request bc attempts is {request_attempts} and games played is {games_played}", + ) + return True + + # if games_played is zero return true + if games_played == 0: + self.log( + f"Can request bc attempts is {request_attempts} and games played is {games_played}", + ) + return True + + # if games_played / increment > request_attempts + if games_played / increment >= request_attempts: + self.log( + f"Can request. attempts = {request_attempts} & games played = {games_played}", + ) + return True + + self.log(f"Can't request. {games_played} Games and {request_attempts} Attempts") + return False + + def check_if_can_donate(self, increment) -> bool: + increment = int(increment) + if increment <= 1: + self.log(f"Increment is {increment} so can always donate") + return True + + # count donates + donate_attempts = self.donate_attempts + + # count games + games_played = self._1v1_fights + self._2v2_fights + self.war_fights + + # if donate_attempts is zero return true + if donate_attempts == 0: + self.log( + f"Can donate bc attempts is {donate_attempts} and games played is {games_played}", + ) + return True + + # if games_played is zero return true + if games_played == 0: + self.log( + f"Can donate bc attempts is {donate_attempts} and games played is {games_played}", + ) + return True + + # if games_played / increment > donate_attempts + if games_played / increment >= donate_attempts: + self.change_status( + f"Can donate. attempts = {donate_attempts} & games played = {games_played}", + ) + return True + + self.log(f"Can't donate. {games_played} games and {donate_attempts} Attempts") + return False + + def check_if_can_shop_buy(self, increment) -> bool: + """Method to check if can collect free offers given + attempts, games played, and user increment input + """ + increment = int(increment) + if increment <= 1: + self.log(f"Increment is {increment} so can always Collect Free Offers") + return True + + # count shop_buy_attempts + shop_buy_attempts = self.shop_buy_attempts + + # count games + games_played = self._1v1_fights + self._2v2_fights + self.war_fights + + # if shop_buy_attempts is zero return true + if shop_buy_attempts == 0: + self.log( + f"Can collect shop_buy. {games_played} Games and {shop_buy_attempts} Attempts", + ) + return True + + # if games_played is zero return true + if games_played == 0: + self.log( + f"Can collect shop_buy. {games_played} Games and {shop_buy_attempts} Attempts", + ) + return True + + # if games_played / increment > shop_buy_attempts + if games_played / increment >= shop_buy_attempts: + self.log( + f"Can collect shop_buy. {games_played} Games and {shop_buy_attempts} Attempts", + ) + return True + + self.log( + f"Can't do shop_buy . {games_played} Games and {shop_buy_attempts} Attempts", + ) + return False + + def check_if_can_battlepass_collect(self, increment) -> bool: + """Method to check if can collect battlepass given + attempts, games played, and user increment input + """ + increment = int(increment) + if increment <= 1: + self.log(f"Increment is {increment} so can always Collect Free Offers") + return True + + # count battlepass_collect_attempts + battlepass_collect_attempts = self.battlepass_collect_attempts + + # count games + games_played = self._1v1_fights + self._2v2_fights + self.war_fights + + # if shop_buy_attempts is zero return true + if battlepass_collect_attempts == 0: + self.log( + f"Can collect battlepass_collect. {games_played} Games and {battlepass_collect_attempts} Attempts", + ) + return True + + # if games_played is zero return true + if games_played == 0: + self.log( + f"Can collect battlepass_collect. {games_played} Games and {battlepass_collect_attempts} Attempts", + ) + return True + + # if games_played / increment > battlepass_collect_attempts + if games_played / increment >= battlepass_collect_attempts: + self.log( + f"Can collect battlepass_collect. {games_played} Games and {battlepass_collect_attempts} Attempts", + ) + return True + + self.log( + f"Can't do battlepass_collect . {games_played} Games and {battlepass_collect_attempts} Attempts", + ) + return False + + def check_if_can_collect_daily_rewards(self, increment) -> bool: + """Method to check if can collect free offers given + attempts, games played, and user increment input + """ + increment = int(increment) + if increment <= 1: + self.log(f"Increment is {increment} so can always Collect Free Offers") + return True + + # count daily_reward_attempts + daily_reward_attempts = self.daily_reward_attempts + + # count games + games_played = self._1v1_fights + self._2v2_fights + self.war_fights + + # if daily_reward_attempts is zero return true + if daily_reward_attempts == 0: + self.log( + f"Can collect collect_daily_rewards. {games_played} Games and {daily_reward_attempts} Attempts", + ) + return True + + # if games_played is zero return true + if games_played == 0: + self.log( + f"Can collect collect_daily_rewards. {games_played} Games and {daily_reward_attempts} Attempts", + ) + return True + + # if games_played / increment > shop_buy_attempts + if games_played / increment >= daily_reward_attempts: + self.log( + f"Can collect collect_daily_rewards. {games_played} Games and {daily_reward_attempts} Attempts", + ) + return True + + self.log( + f"Can't do collect_daily_rewards . {games_played} Games and {daily_reward_attempts} Attempts", + ) + return False + + def check_if_can_randomize_deck(self, increment): + """Method to check if can randomize deck given + attempts, games played, and user increment input + """ + increment = int(increment) + if increment <= 1: + self.log(f"Increment is {increment} so can always randomize deck") + return True + + # count free_offer_collection_attempts + deck_randomize_attempts = self.deck_randomize_attempts + + # count games + games_played = self._1v1_fights + self._2v2_fights + self.war_fights + + # if deck_randomize_attempts is zero return true + if deck_randomize_attempts == 0: + self.log( + f"Can randomize deck. {games_played} Games and {deck_randomize_attempts} Attempts", + ) + return True + + # if games_played is zero return true + if games_played == 0: + self.log( + f"Can randomize deck. {games_played} Games and {deck_randomize_attempts} Attempts", + ) + return True + + # if games_played / increment > deck_randomize_attempts + if games_played / increment >= deck_randomize_attempts: + self.log( + f"Can randomize deck. {games_played} Games and {deck_randomize_attempts} Attempts", + ) + return True + + self.log( + f"Can't randomize deck. {games_played} Games and {deck_randomize_attempts} Attempts", + ) + return False + + def check_if_can_switch_account(self, increment): + """Method to check if can switch account given + attempts, games played, and user increment input + """ + if increment == 1: + self.log(f"Increment is {increment} so can always switch account") + return True + + # count free_offer_collection_attempts + switch_account_attempts = self.switch_account_attempts + + # count games + games_played = self._1v1_fights + self._2v2_fights + self.war_fights + + # if games_played or switch accounts attempts is zero return true + if games_played == 0: + self.log( + f"Can switch account. {games_played} Games and {switch_account_attempts} Attempts", + ) + return True + + # if games_played / int(increment) > switch accounts attempts + if games_played / int(increment) >= switch_account_attempts: + self.log( + f"Can switch account. {games_played} Games and {switch_account_attempts} Attempts", + ) + return True + + self.log( + f"Can't switch account. {games_played} Games and {switch_account_attempts} Attempts", + ) + return False + + def check_if_can_buy_season_shop_offers(self, increment) -> bool: + """Method to check if can switch account given + attempts, games played, and user increment input + """ + if increment == 1: + self.log(f"Increment is {increment} so can always switch account") + return True + + season_shop_buys_attempts = self.season_shop_buys_attempts + + games_played = self._1v1_fights + self._2v2_fights + self.war_fights + + if games_played == 0: + self.log( + f"Can buy_season_shop_offers. {games_played} Games and {season_shop_buys_attempts} Attempts", + ) + return True + + if games_played / int(increment) >= season_shop_buys_attempts: + self.log( + f"Can buy_season_shop_offers. {games_played} Games and {season_shop_buys_attempts} Attempts", + ) + return True + + self.log( + f"Can't buy_season_shop_offers. {games_played} Games and {season_shop_buys_attempts} Attempts", + ) + return False + + def update_time_of_last_request(self, input_time) -> None: + """Sets logger's time_of_last_request to input_time""" + self.time_of_last_request = input_time + + def set_total_accounts(self, count): + self.total_accounts = count + def update_time_of_last_card_upgrade(self, input_time) -> None: """Sets logger's time_of_last_card_upgrade to input_time""" self.time_of_last_card_upgrade = input_time @@ -553,10 +1097,10 @@ def upload_log(self) -> str | None: """Method to upload log to pastebin""" with open(log_name, encoding="utf-8") as log_file: return upload_pastebin( - f"py-clash-bot log ({basename(log_name)})", - log_file.read(), + f"py-clash-bot log ({basename(log_name)})", log_file.read(), ) if __name__ == "__main__": - pass + initalize_pylogging() + logger = Logger() diff --git a/src/pyproject.toml b/src/pyproject.toml index e16d8610d..e05ddaa4e 100644 --- a/src/pyproject.toml +++ b/src/pyproject.toml @@ -19,14 +19,14 @@ include = [ [tool.poetry.dependencies] python = ">=3.12.0,<3.13" opencv-python = "^4.9.0" -numpy = "^2.1.2" +numpy = "^1.26.4" pysimplegui = "^4.0.0" -pymemuc = "^0.6.0" -psutil = "^6.1.0" +pymemuc = "^0.5.5" +psutil = "^5.9.8" requests = "^2.31.0" pygetwindow = "^0.0.9" pyautogui = "^0.9.54" -pygame = "^2.6.1" +pygame = "^2.5.2" [tool.poetry.group.build] @@ -34,8 +34,8 @@ optional = true [tool.poetry.group.build.dependencies] -cx-freeze = "^7.2.5" -poethepoet = "^0.30.0" +cx-freeze = "^7.2.0" +poethepoet = "^0.25.0" [tool.poetry.group.dev] @@ -43,15 +43,15 @@ optional = true [tool.poetry.group.dev.dependencies] -pre-commit = "^4.0.1" -vulture = "^2.13" +pre-commit = "^3.7.0" +vulture = "^2.11" isort = "^5.13.0" -poethepoet = "^0.30.0" -notebook = "^7.2.2" +poethepoet = "^0.25.0" +notebook = "^7.1.2" ipykernel = "^6.29.2" matplotlib = "^3.8.3" decorator = "^5.1.1" -ruff = "^0.7.4" +ruff = "^0.6.1" [tool.poe.tasks.build-dist] @@ -104,7 +104,7 @@ select = ["ALL"] [tool.ruff] -line-length = 120 +line-length = 88 [tool.isort]