├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── README.rst ├── Tools ├── AdbWinApi.dll ├── AdbWinUsbApi.dll ├── README.md ├── adb.exe └── fastboot.exe ├── changelog.md ├── common ├── UnicodeStreamFilter.py ├── __init__.py ├── ai.py ├── auto_adb.py ├── config.py ├── debug.py └── screenshot.py ├── config ├── 1280x720 │ └── config.json ├── 1440x720 │ └── config.json ├── 1800x1080 │ └── config.json ├── 1920x1080 │ └── config.json ├── 2160x1080 │ └── config.json ├── 2560x1440 │ └── config.json ├── 720x1280 ├── 960x540 │ └── config.json ├── default.json ├── huawei │ ├── honorV8 │ └── honor_note8_config.json ├── iPhone │ ├── 6_config.json │ ├── 8P_7P_6sP_6P_config.json │ ├── 8_config.json │ ├── SE_config.json │ └── X_config.json ├── mi │ ├── max2_config.json │ ├── mi5_config.json │ ├── mi5s_config.json │ ├── mi5x_config.json │ ├── mi6_config.json │ ├── mix2_config.json │ └── note2_config.json ├── samsung │ ├── s7edge_config.json │ ├── s8.json │ └── s8在设置里关闭曲面侧屏 └── smartisan │ └── pro2_config.json ├── jump_bot ├── Makefile ├── README.rst ├── jumpbot │ ├── algos.py │ ├── auto.py │ ├── bot.py │ ├── connector.py │ ├── manual.py │ └── settings.py └── requirements.txt ├── requirements.txt ├── resource ├── image │ ├── character.png │ ├── jump.gif │ └── qrcode_for_gh_3586401957c4_258.jpg └── model │ ├── checkpoint │ ├── model.ckpt.data-00000-of-00001 │ ├── model.ckpt.index │ └── model.ckpt.meta ├── wechat_jump.py ├── wechat_jump_auto.py ├── wechat_jump_auto_ai.py ├── wechat_jump_auto_curves.py ├── wechat_jump_auto_iOS.py ├── wechat_jump_auto_slim.py ├── wechat_jump_iOS_py3.py └── wechat_jump_py3.py /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangshub/wechat_jump_game/3cbbb07957901821542fcfc231a09b4f3c9318c9/.github/CONTRIBUTING.md -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 21 | 22 | 程序版本号:x.x.x 23 | ********** 24 | Screen: Physical size: x 25 | Density: Physical density: x 26 | Override density: x 27 | Device: x 28 | Phone OS: x 29 | Host OS: x 30 | Python: x 31 | ********** 32 | 33 | 遇到问题: 34 | 35 | 改进和建议:无 36 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 20 | 21 | 本次 PR 主要做的事情: 22 | 23 | - x 24 | 25 | 修改后最高分数:x 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # user-defined 2 | test/ 3 | pic/ 4 | screenshot_backups/ 5 | autojump.png 6 | autojump_temp.png 7 | region*.png 8 | jump_range.csv 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | .idea 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | .hypothesis/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | .static_storage/ 66 | .media/ 67 | local_settings.py 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # celery beat schedule file 89 | celerybeat-schedule 90 | 91 | # SageMath parsed files 92 | *.sage.py 93 | 94 | # Environments 95 | .env 96 | .venv 97 | env/ 98 | venv/ 99 | ENV/ 100 | env.bak/ 101 | venv.bak/ 102 | 103 | # Spyder project settings 104 | .spyderproject 105 | .spyproject 106 | 107 | # Rope project settings 108 | .ropeproject 109 | 110 | # mkdocs documentation 111 | /site 112 | 113 | # mypy 114 | .mypy_cache/ 115 | 116 | # for mac 117 | .DS_store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [wangshub] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | mkdir -p jumpbot/data 3 | python3 jumpbot/bot.py 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 教你用 Python 来玩微信跳一跳 2 | [![GitHub stars](https://img.shields.io/github/stars/wangshub/wechat_jump_game.svg)](https://github.com/wangshub/wechat_jump_game/stargazers) [![GitHub forks](https://img.shields.io/github/forks/wangshub/wechat_jump_game.svg)](https://github.com/wangshub/wechat_jump_game/network) [![GitHub license](https://img.shields.io/github/license/wangshub/wechat_jump_game.svg)](https://github.com/wangshub/wechat_jump_game/blob/master/LICENSE) 3 | 4 | [![Throughput Graph](https://graphs.waffle.io/wangshub/wechat_jump_game/throughput.svg)](https://waffle.io/wangshub/wechat_jump_game/metrics/throughput) 5 | 6 | ## 游戏模式 7 | 8 | > 2017 年 12 月 28 日下午,微信发布了 6.6.1 版本,加入了「小游戏」功能,并提供了官方 DEMO「跳一跳」。这是一个 2.5D 插画风格的益智游戏,玩家可以通过按压屏幕时间的长短来控制这个「小人」跳跃的距离。分数越高,那么在好友排行榜更加靠前。通过 Python 脚本自动运行,让你轻松霸榜。 9 | 10 | ![](./resource/image/jump.gif) 11 | 12 | 可能刚开始上手的时候,因为时间距离之间的关系把握不恰当,只能跳出几个就掉到了台子下面。**如果能利用图像识别精确测量出起始和目标点之间测距离,就可以估计按压的时间来精确跳跃。** 13 | 14 | ## 原理说明 15 | 16 | ##### 由于微信检测非常严厉,这里的防禁代码可能已经不起作用,主要供学习用途 17 | 18 | 1. 将手机点击到《跳一跳》小程序界面 19 | 20 | 2. 用 ADB 工具获取当前手机截图,并用 ADB 将截图 pull 上来 21 | ```shell 22 | adb shell screencap -p /sdcard/autojump.png 23 | adb pull /sdcard/autojump.png . 24 | ``` 25 | 26 | 3. 计算按压时间 27 | * 手动版:用 Matplotlib 显示截图,用鼠标先点击起始点位置,然后点击目标位置,计算像素距离; 28 | * 自动版:靠棋子的颜色来识别棋子,靠底色和方块的色差来识别棋盘; 29 | 30 | 4. 用 ADB 工具点击屏幕蓄力一跳 31 | ```shell 32 | adb shell input swipe x y x y time(ms) 33 | ``` 34 | 35 | 36 | 37 | ## 使用教程 38 | 39 | 相关软件工具安装和使用步骤请参考 [Android 和 iOS 操作步骤](https://github.com/wangshub/wechat_jump_game/wiki/Android-%E5%92%8C-iOS-%E6%93%8D%E4%BD%9C%E6%AD%A5%E9%AA%A4) 40 | 41 | #### 获取源码 42 | 43 | ``` 44 | - git clone https://github.com/wangshub/wechat_jump_game.git 45 | 46 | ``` 47 | ##### 非常推荐使用Python3,避免编码及import问题 48 | ## PR 要求 49 | ##### 请选择 merge 进 master 分支,并且标题写上简短描述,例子 50 | [优化] 使用PEP8优化代码 51 | 52 | ## 版本说明 53 | 54 | - master 分支:稳定版本,已通过测试 55 | - dev 分支:开发版本,包含一些较稳定的新功能,累计多个功能并测试通过后合并至 prod 分支 56 | - 其他分支:功能开发 (feature) 或问题修复 (bugfix),属于最新尝鲜版本,可能处于开发中的状态,基本完成后合并至 dev 分支 57 | 58 | ## FAQ 59 | 60 | - 详见 [Wiki-FAQ](https://github.com/wangshub/wechat_jump_game/wiki/FAQ) 61 | 62 | ## 更新日志 63 | 64 | - 详见 [changelog](https://github.com/wangshub/wechat_jump_game/blob/master/changelog.md) 65 | 66 | ## 开发者列表 67 | 68 | - 详见 [contributors](https://github.com/wangshub/wechat_jump_game/graphs/contributors) 69 | 70 | ## 交流 71 | 72 | - 314659953 (1000 人) 73 | - 176740763 (500 人) 74 | 75 | - 或者关注我的微信公众号后台留言 76 | 77 | ![](./resource/image/qrcode_for_gh_3586401957c4_258.jpg) 78 | 79 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | Wechat Jump Bot (iOS) 3 | ############################################################################## 4 | 5 | ============================================================================== 6 | Features 7 | ============================================================================== 8 | 9 | - Auto Mode: play the game automatically; 10 | - Manual Mode: play the game manually. 11 | 12 | Wechat Jump Game 13 | 14 | .. image:: https://github.com/alpesis-ai/wechat-jumpbot/blob/master/images/auto.png 15 | :height: 1334px 16 | :width: 500px 17 | 18 | 19 | ============================================================================== 20 | How it runs 21 | ============================================================================== 22 | 23 | Prerequisites 24 | 25 | - WebDriverAgent 26 | - libimobiledevice 27 | - Python 3 28 | 29 | WebDriverAgent 30 | 31 | :: 32 | 33 | $ git clone https://github.com/facebook/WebDriverAgent && cd WebDriverAgent 34 | $ brew install carthage 35 | $ ./Scripts/bootstrap.sh 36 | # open WebDriverAgent.xcodeproj with Xcode 37 | # Xcode: 38 | # - code sign (general and build_settings): WebDriverAgentLib/WebDriverAgentRunner 39 | # - Product -> Destination -> 40 | # - Product -> Scheme -> WebDriverAgentRunner 41 | # - Product -> Test 42 | 43 | libimobiledevice (iproxy) 44 | 45 | :: 46 | 47 | $ brew install libimobiledevice 48 | $ iproxy 8100 8100 49 | # browse: http://localhost:8100/status 50 | # browse: http://localhost:8100/inspector 51 | 52 | Bot Agent (iOS) 53 | 54 | :: 55 | 56 | $ git clone https://github.com/alpesis-ai/wechat-jumpbot.git 57 | $ cd bot-agent-ios 58 | 59 | $ pip3 install --pre facebook-wda 60 | $ pip3 install -r requirements.txt 61 | 62 | # make run 63 | # - model: [ip, plus, ipx, se] 64 | # - mode: [auto, manual] 65 | # python3 jumpbot/bot.py --model --mode 66 | $ mkdir -p jumpbot/data 67 | # iphone 6/7 68 | $ python3 jumpbot/bot.py --model ip --mode auto 69 | # iphone 6/7 plus 70 | $ python3 jumpbot/bot.py --model plus --mode auto 71 | # iphone X 72 | $ python3 jumpbot/bot.py --model ipx --mode auto 73 | # iphone SE 74 | $ python3 jumpbot/bot.py --model se --mode auto 75 | 76 | 77 | ============================================================================== 78 | Algorithms 79 | ============================================================================== 80 | 81 | Manual Mode: 82 | 83 | - click the piece(x, y) and board(x, y) and get the coordinates correspondingly 84 | - calculating the distance and press time 85 | 86 | :: 87 | 88 | (coord1[0][0] - coord2[0][0])**2 + (coord2[0][1] - coord2[0][1])**2 89 | distance = distance ** 0.5 90 | press_time = distance * settings.TIME_COEFF 91 | 92 | Auto Mode: 93 | 94 | - the main idea same as the manual mode, but detecting the piece and the board automatically 95 | - find coord_y_start_scan 96 | - find piece 97 | - find board 98 | 99 | 100 | ============================================================================== 101 | Developement 102 | ============================================================================== 103 | 104 | :: 105 | 106 | connector ---| 107 | | --> auto / manual --> bot 108 | algos ---| 109 | -------------------------------------------------------------------------------- /Tools/AdbWinApi.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangshub/wechat_jump_game/3cbbb07957901821542fcfc231a09b4f3c9318c9/Tools/AdbWinApi.dll -------------------------------------------------------------------------------- /Tools/AdbWinUsbApi.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangshub/wechat_jump_game/3cbbb07957901821542fcfc231a09b4f3c9318c9/Tools/AdbWinUsbApi.dll -------------------------------------------------------------------------------- /Tools/README.md: -------------------------------------------------------------------------------- 1 | ## 所有实验都在该文件夹下运行即可,已放上测试用的代码。免去配置 adb 的麻烦 2 | 3 | - 复制 wechat_jump_game 根目录下的 config 文件夹以及 wechat_jump_py3.py 文件到本目录下 4 | - 按住 `shift` + 右键 选择在该文件夹下打开命令窗口(Windows 用户请自行 cmd) 5 | - 打开安卓手机的 usb 调试,并连接电脑,在终端输入 `adb devices` 进行测试,如果有连接设备号则表示成功 6 | - 打开微信小游戏,然后运行代码 `python wechat_jump_py3.py`,点击出现的图形起点和终点,棋子自动跳转 7 | 8 | **注意:这里使用的是不需要配置的 adb 方式,需要在该文件下操作,至于如何自动跳转,只需改变执行脚本即可,这里只做演示** 9 | -------------------------------------------------------------------------------- /Tools/adb.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangshub/wechat_jump_game/3cbbb07957901821542fcfc231a09b4f3c9318c9/Tools/adb.exe -------------------------------------------------------------------------------- /Tools/fastboot.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangshub/wechat_jump_game/3cbbb07957901821542fcfc231a09b4f3c9318c9/Tools/fastboot.exe -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## 更新日志 2 | - 2018-1-8 3 | - 发布 release 一键启动 app V1.1.2 版本,针对腾讯 ban 方法进行反 ban 操作,详见 [STOP_jump](https://github.com/wangshub/wechat_jump_game/releases) 4 | 5 | - 2018-1-3 : 6 | - 发布 release 一键启动 app,详见 [STOP_jump](https://github.com/wangshub/wechat_jump_game/releases) 7 | 8 | - 2017-12-30 : 9 | - 请将安卓手机的 USB 调试模式打开,设置 > 更多设置 > 开发者选项 > USB 调试,如果出现运行脚本后小人不跳的情况,请检查是否有打开 “USB 调试(安全模式)” 10 | - 根据大家反馈:1080 屏幕距离系数 **1.393**,2K 屏幕为 **1** 11 | - 添加部分机型配置文件,可直接复制使用 12 | 13 | - 2017-12-29 : 14 | - 增加更新自动化运行脚本,感谢 GitHub 上的 [@binderclip](https://github.com/binderclip) 15 | -------------------------------------------------------------------------------- /common/UnicodeStreamFilter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | 4 | if sys.version_info.major != 3: 5 | class UnicodeStreamFilter: 6 | 7 | def __init__(self, target): 8 | self.target = target 9 | self.encoding = 'utf-8' 10 | self.errors = 'replace' 11 | self.encode_to = self.target.encoding 12 | 13 | 14 | def write(self, s): 15 | if type(s) == str: 16 | s = s.decode("utf-8") 17 | s = s.encode(self.encode_to, self.errors).decode(self.encode_to) 18 | self.target.write(s) 19 | 20 | 21 | if sys.stdout.encoding == 'cp936': 22 | sys.stdout = UnicodeStreamFilter(sys.stdout) 23 | else: 24 | pass 25 | -------------------------------------------------------------------------------- /common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangshub/wechat_jump_game/3cbbb07957901821542fcfc231a09b4f3c9318c9/common/__init__.py -------------------------------------------------------------------------------- /common/ai.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # Copyright (c) 2018 BeiTown 4 | 5 | import os 6 | import pandas 7 | from sklearn.linear_model import LinearRegression 8 | 9 | 10 | def linear_model_main(_distances, _press_times, target_distance): 11 | regr = LinearRegression() 12 | regr.fit(_distances, _press_times) 13 | predict_press_time = regr.predict(target_distance) 14 | result = {} 15 | # 截距 b 16 | result['intercept'] = regr.intercept_ 17 | # 斜率值 k 18 | result['coefficient'] = regr.coef_ 19 | # 预估的按压时间 20 | result['value'] = predict_press_time 21 | return result 22 | 23 | 24 | def computing_k_b_v(target_distance): 25 | result = linear_model_main(distances, press_times, target_distance) 26 | b = result['intercept'] 27 | k = result['coefficient'] 28 | v = result['value'] 29 | return k[0], b[0], v[0] 30 | 31 | 32 | def add_data(distance, press_time): 33 | distances.append([distance]) 34 | press_times.append([press_time]) 35 | save_data('./jump_range.csv', distances, press_times) 36 | 37 | 38 | def save_data(file_name, distances, press_times): 39 | pf = pandas.DataFrame({'Distance': distances, 'Press_time': press_times}) 40 | # print(pf) 41 | pf.to_csv(file_name, index=False, sep=',') 42 | 43 | 44 | def get_data(file_name): 45 | data = pandas.read_csv(file_name) 46 | distance_array = [] 47 | press_time_array = [] 48 | for distance, press_time in zip(data['Distance'], data['Press_time']): 49 | distance_array.append([float(distance.strip().strip('[]'))]) 50 | press_time_array.append([float(press_time.strip().strip('[]'))]) 51 | return distance_array, press_time_array 52 | 53 | 54 | def init(): 55 | global distances, press_times 56 | distances = [] 57 | press_times = [] 58 | 59 | if os.path.exists('./jump_range.csv'): 60 | distances, press_times = get_data('./jump_range.csv') 61 | else: 62 | save_data('./jump_range.csv', [], []) 63 | return 0 64 | 65 | 66 | def get_result_len(): 67 | return len(distances) 68 | -------------------------------------------------------------------------------- /common/auto_adb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import subprocess 4 | import platform 5 | 6 | 7 | class auto_adb(): 8 | def __init__(self): 9 | try: 10 | adb_path = 'adb' 11 | subprocess.Popen([adb_path], stdout=subprocess.PIPE, 12 | stderr=subprocess.PIPE) 13 | self.adb_path = adb_path 14 | except OSError: 15 | if platform.system() == 'Windows': 16 | adb_path = os.path.join('Tools', "adb", 'adb.exe') 17 | try: 18 | subprocess.Popen( 19 | [adb_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 20 | self.adb_path = adb_path 21 | except OSError: 22 | pass 23 | else: 24 | try: 25 | subprocess.Popen( 26 | [adb_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 27 | except OSError: 28 | pass 29 | print('请安装 ADB 及驱动并配置环境变量') 30 | print('具体链接: https://github.com/wangshub/wechat_jump_game/wiki') 31 | exit(1) 32 | 33 | def get_screen(self): 34 | process = os.popen(self.adb_path + ' shell wm size') 35 | output = process.read() 36 | return output 37 | 38 | def run(self, raw_command): 39 | command = '{} {}'.format(self.adb_path, raw_command) 40 | process = os.popen(command) 41 | output = process.read() 42 | return output 43 | 44 | def test_device(self): 45 | print('检查设备是否连接...') 46 | command_list = [self.adb_path, 'devices'] 47 | process = subprocess.Popen(command_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 48 | output = process.communicate() 49 | if output[0].decode('utf8') == 'List of devices attached\n\n': 50 | print('未找到设备') 51 | print('adb 输出:') 52 | for each in output: 53 | print(each.decode('utf8')) 54 | exit(1) 55 | print('设备已连接') 56 | print('adb 输出:') 57 | for each in output: 58 | print(each.decode('utf8')) 59 | 60 | def test_density(self): 61 | process = os.popen(self.adb_path + ' shell wm density') 62 | output = process.read() 63 | return output 64 | 65 | def test_device_detail(self): 66 | process = os.popen(self.adb_path + ' shell getprop ro.product.device') 67 | output = process.read() 68 | return output 69 | 70 | def test_device_os(self): 71 | process = os.popen(self.adb_path + ' shell getprop ro.build.version.release') 72 | output = process.read() 73 | return output 74 | 75 | def adb_path(self): 76 | return self.adb_path 77 | -------------------------------------------------------------------------------- /common/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 调取配置文件和屏幕分辨率的代码 4 | """ 5 | import os 6 | import sys 7 | import json 8 | import re 9 | 10 | from common.auto_adb import auto_adb 11 | 12 | adb = auto_adb() 13 | 14 | 15 | def open_accordant_config(): 16 | """ 17 | 调用配置文件 18 | """ 19 | screen_size = _get_screen_size() 20 | config_file = "{path}/config/{screen_size}/config.json".format( 21 | path=sys.path[0], 22 | screen_size=screen_size 23 | ) 24 | 25 | # 优先获取执行文件目录的配置文件 26 | here = sys.path[0] 27 | for file in os.listdir(here): 28 | if re.match(r'(.+)\.json', file): 29 | file_name = os.path.join(here, file) 30 | with open(file_name, 'r') as f: 31 | print("Load config file from {}".format(file_name)) 32 | return json.load(f) 33 | 34 | # 根据分辨率查找配置文件 35 | if os.path.exists(config_file): 36 | with open(config_file, 'r') as f: 37 | print("正在从 {} 加载配置文件".format(config_file)) 38 | return json.load(f) 39 | else: 40 | with open('{}/config/default.json'.format(sys.path[0]), 'r') as f: 41 | print("Load default config") 42 | return json.load(f) 43 | 44 | 45 | def _get_screen_size(): 46 | """ 47 | 获取手机屏幕大小 48 | """ 49 | size_str = adb.get_screen() 50 | m = re.search(r'(\d+)x(\d+)', size_str) 51 | if m: 52 | return "{height}x{width}".format(height=m.group(2), width=m.group(1)) 53 | return "1920x1080" 54 | -------------------------------------------------------------------------------- /common/debug.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 这是debug的代码,当DEBUG_SWITCH开关开启的时候,会将各种信息存在本地,方便检查故障 4 | """ 5 | import os 6 | import sys 7 | import shutil 8 | import math 9 | from PIL import ImageDraw 10 | import platform 11 | if platform.system() == 'Windows': 12 | os.chdir(os.getcwd().replace('\\common', '')) 13 | path_split = "\\" 14 | else: 15 | os.chdir(os.getcwd().replace('/common', '')) 16 | path_split = '/' 17 | # from common import ai 18 | try: 19 | from common.auto_adb import auto_adb 20 | except ImportError as ex: 21 | print(ex) 22 | print('请将脚本放在项目根目录中运行') 23 | print('请检查项目根目录中的 common 文件夹是否存在') 24 | exit(1) 25 | screenshot_backup_dir = 'screenshot_backups' 26 | adb = auto_adb() 27 | 28 | 29 | def make_debug_dir(screenshot_backup_dir): 30 | """ 31 | 创建备份文件夹 32 | """ 33 | if not os.path.isdir(screenshot_backup_dir): 34 | os.mkdir(screenshot_backup_dir) 35 | 36 | 37 | def backup_screenshot(ts): 38 | """ 39 | 为了方便失败的时候 debug 40 | """ 41 | make_debug_dir(screenshot_backup_dir) 42 | shutil.copy('{}{}autojump.png'.format(os.getcwd(), path_split), 43 | os.path.join(os.getcwd(), screenshot_backup_dir, 44 | str(ts) + '.png')) 45 | 46 | 47 | def save_debug_screenshot(ts, im, piece_x, piece_y, board_x, board_y): 48 | """ 49 | 对 debug 图片加上详细的注释 50 | 51 | """ 52 | make_debug_dir(screenshot_backup_dir) 53 | draw = ImageDraw.Draw(im) 54 | draw.line((piece_x, piece_y) + (board_x, board_y), fill=2, width=3) 55 | draw.line((piece_x, 0, piece_x, im.size[1]), fill=(255, 0, 0)) 56 | draw.line((0, piece_y, im.size[0], piece_y), fill=(255, 0, 0)) 57 | draw.line((board_x, 0, board_x, im.size[1]), fill=(0, 0, 255)) 58 | draw.line((0, board_y, im.size[0], board_y), fill=(0, 0, 255)) 59 | draw.ellipse((piece_x - 10, piece_y - 10, piece_x + 10, piece_y + 10), fill=(255, 0, 0)) 60 | draw.ellipse((board_x - 10, board_y - 10, board_x + 10, board_y + 10), fill=(0, 0, 255)) 61 | del draw 62 | im.save(os.path.join(os.getcwd(), screenshot_backup_dir, 63 | '#' + str(ts) + '.png')) 64 | 65 | 66 | def computing_error(last_press_time, target_board_x, target_board_y, last_piece_x, last_piece_y, temp_piece_x, 67 | temp_piece_y): 68 | """ 69 | 计算跳跃实际误差 70 | """ 71 | target_distance = math.sqrt( 72 | (target_board_x - last_piece_x) ** 2 + (target_board_y - last_piece_y) ** 2) # 上一轮目标跳跃距离 73 | actual_distance = math.sqrt((temp_piece_x - last_piece_x) ** 2 + (temp_piece_y - last_piece_y) ** 2) # 上一轮实际跳跃距离 74 | jump_error_value = math.sqrt((target_board_x - temp_piece_x) ** 2 + (target_board_y - temp_piece_y) ** 2) # 跳跃误差 75 | 76 | print(round(target_distance), round(jump_error_value), round(actual_distance), round(last_press_time)) 77 | ''''# 将结果采集进学习字典 78 | if last_piece_x > 0 and last_press_time > 0: 79 | ai.add_data(round(actual_distance, 2), round(last_press_time)) 80 | # print(round(actual_distance), round(last_press_time))''' 81 | 82 | 83 | def dump_device_info(): 84 | """ 85 | 显示设备信息 86 | """ 87 | size_str = adb.get_screen() 88 | device_str = adb.test_device_detail() 89 | phone_os_str = adb.test_device_os() 90 | density_str = adb.test_density() 91 | print("""********** 92 | Screen: {size} 93 | Density: {dpi} 94 | Device: {device} 95 | Phone OS: {phone_os} 96 | Host OS: {host_os} 97 | Python: {python} 98 | **********""".format( 99 | size=size_str.replace('\n', ''), 100 | dpi=density_str.replace('\n', ''), 101 | device=device_str.replace('\n', ''), 102 | phone_os=phone_os_str.replace('\n', ''), 103 | host_os=sys.platform, 104 | python=sys.version 105 | )) 106 | -------------------------------------------------------------------------------- /common/screenshot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 手机屏幕截图的代码 4 | """ 5 | import subprocess 6 | import os 7 | import sys 8 | from PIL import Image 9 | from io import StringIO 10 | 11 | try: 12 | from common.auto_adb import auto_adb 13 | except Exception as ex: 14 | print(ex) 15 | print('请将脚本放在项目根目录中运行') 16 | print('请检查项目根目录中的 common 文件夹是否存在') 17 | exit(1) 18 | adb = auto_adb() 19 | # SCREENSHOT_WAY 是截图方法,经过 check_screenshot 后,会自动递减,不需手动修改 20 | SCREENSHOT_WAY = 3 21 | 22 | 23 | def pull_screenshot(): 24 | """ 25 | 获取屏幕截图,目前有 0 1 2 3 四种方法,未来添加新的平台监测方法时, 26 | 可根据效率及适用性由高到低排序 27 | """ 28 | global SCREENSHOT_WAY 29 | if 1 <= SCREENSHOT_WAY <= 3: 30 | process = subprocess.Popen( 31 | adb.adb_path + ' shell screencap -p', 32 | shell=True, stdout=subprocess.PIPE) 33 | binary_screenshot = process.stdout.read() 34 | if SCREENSHOT_WAY == 2: 35 | binary_screenshot = binary_screenshot.replace(b'\r\n', b'\n') 36 | elif SCREENSHOT_WAY == 1: 37 | binary_screenshot = binary_screenshot.replace(b'\r\r\n', b'\n') 38 | return Image.open(StringIO(binary_screenshot)) 39 | elif SCREENSHOT_WAY == 0: 40 | adb.run('shell screencap -p /sdcard/autojump.png') 41 | adb.run('pull /sdcard/autojump.png .') 42 | return Image.open('./autojump.png') 43 | 44 | 45 | def check_screenshot(): 46 | """ 47 | 检查获取截图的方式 48 | """ 49 | global SCREENSHOT_WAY 50 | if os.path.isfile('autojump.png'): 51 | try: 52 | os.remove('autojump.png') 53 | except Exception: 54 | pass 55 | if SCREENSHOT_WAY < 0: 56 | print('暂不支持当前设备') 57 | sys.exit() 58 | try: 59 | im = pull_screenshot() 60 | im.load() 61 | im.close() 62 | print('采用方式 {} 获取截图'.format(SCREENSHOT_WAY)) 63 | except Exception: 64 | SCREENSHOT_WAY -= 1 65 | check_screenshot() 66 | -------------------------------------------------------------------------------- /config/1280x720/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 200, 3 | "press_coefficient": 2.099, 4 | "piece_base_height_1_2": 13, 5 | "piece_body_width": 47, 6 | "swipe": { 7 | "x1": 374, 8 | "y1": 1060, 9 | "x2": 374, 10 | "y2": 1060 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/1440x720/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 200, 3 | "press_coefficient": 2.099, 4 | "piece_base_height_1_2": 13, 5 | "piece_body_width": 47, 6 | "swipe" : { 7 | "x1": 360, 8 | "y1": 1140, 9 | "x2": 360, 10 | "y2": 1142 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/1800x1080/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 300, 3 | "press_coefficient": 1.2, 4 | "piece_base_height_1_2": 20, 5 | "piece_body_width": 70, 6 | "swipe": { 7 | "x1": 500, 8 | "y1": 1600, 9 | "x2": 500, 10 | "y2": 1602 11 | } 12 | } -------------------------------------------------------------------------------- /config/1920x1080/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 300, 3 | "press_coefficient": 1.392, 4 | "piece_base_height_1_2": 20, 5 | "piece_body_width": 70, 6 | "head_diameter": 60, 7 | "swipe": { 8 | "x1": 500, 9 | "y1": 1600, 10 | "x2": 500, 11 | "y2": 1602 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /config/2160x1080/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 420, 3 | "press_coefficient": 1.372, 4 | "piece_base_height_1_2": 25, 5 | "piece_body_width": 85 6 | } 7 | -------------------------------------------------------------------------------- /config/2560x1440/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 410, 3 | "press_coefficient": 1.475, 4 | "piece_base_height_1_2": 28, 5 | "piece_body_width": 110, 6 | "swipe": { 7 | "x1": 320, 8 | "y1": 410, 9 | "x2": 320, 10 | "y2": 410 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/720x1280: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 369, 3 | "press_coefficient": 2.003, 4 | "piece_base_height_1_2": 13, 5 | "piece_body_width": 45, 6 | "swipe" : { 7 | "x1": 374, 8 | "y1": 1060, 9 | "x2": 374, 10 | "y2": 1060 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/960x540/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 300, 3 | "press_coefficient": 2.732, 4 | "piece_base_height_1_2": 20, 5 | "piece_body_width": 70 6 | } 7 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 300, 3 | "press_coefficient": 1.392, 4 | "piece_base_height_1_2": 20, 5 | "piece_body_width": 70 6 | } 7 | -------------------------------------------------------------------------------- /config/huawei/honorV8: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 400, 3 | "press_coefficient": 1.07, 4 | "piece_base_height_1_2": 90, 5 | "piece_body_width": 120, 6 | "swipe": { 7 | "x1": 730, 8 | "y1": 2100, 9 | "x2": 720, 10 | "y2": 2100 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/huawei/honor_note8_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 400, 3 | "press_coefficient": 1.04, 4 | "piece_base_height_1_2": 90, 5 | "piece_body_width": 120, 6 | "swipe": { 7 | "x1": 730, 8 | "y1": 2100, 9 | "x2": 720, 10 | "y2": 2100 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/iPhone/6_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 200, 3 | "press_coefficient": 1.95, 4 | "piece_base_height_1_2": 13, 5 | "piece_body_width": 49, 6 | "swipe": { 7 | "x1": 375, 8 | "y1": 1055, 9 | "x2": 375, 10 | "y2": 1055 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/iPhone/8P_7P_6sP_6P_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 300, 3 | "press_coefficient": 1.2, 4 | "piece_base_height_1_2": 20, 5 | "piece_body_width": 70, 6 | "swipe": { 7 | "x1": 320, 8 | "y1": 410, 9 | "x2": 320, 10 | "y2": 410 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/iPhone/8_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 200, 3 | "press_coefficient": 1.97, 4 | "piece_base_height_1_2": 13, 5 | "piece_body_width": 50, 6 | "swipe": { 7 | "x1": 375, 8 | "y1": 1200, 9 | "x2": 375, 10 | "y2": 1200 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/iPhone/SE_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 190, 3 | "press_coefficient": 2.3, 4 | "piece_base_height_1_2": 12, 5 | "piece_body_width": 50, 6 | 7 | "swipe" : { 8 | "x1": 375, 9 | "y1": 1055, 10 | "x2": 375, 11 | "y2": 1055 12 | } 13 | } -------------------------------------------------------------------------------- /config/iPhone/X_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 170, 3 | "press_coefficient": 1.31, 4 | "piece_base_height_1_2": 23, 5 | "piece_body_width": 70, 6 | "swipe": { 7 | "x1": 320, 8 | "y1": 410, 9 | "x2": 320, 10 | "y2": 410 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/mi/max2_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 300, 3 | "press_coefficient": 1.5, 4 | "piece_base_height_1_2": 20, 5 | "piece_body_width": 70, 6 | "swipe": { 7 | "x1": 500, 8 | "y1": 1600, 9 | "x2": 500, 10 | "y2": 1600 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/mi/mi5_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 300, 3 | "press_coefficient": 1.475, 4 | "piece_base_height_1_2": 20, 5 | "piece_body_width": 70, 6 | "swipe": { 7 | "x1": 540, 8 | "y1": 1514, 9 | "x2": 540, 10 | "y2": 1514 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/mi/mi5s_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 300, 3 | "press_coefficient": 1.475, 4 | "piece_base_height_1_2": 20, 5 | "piece_body_width": 70, 6 | "swipe": { 7 | "x1": 540, 8 | "y1": 1514, 9 | "x2": 540, 10 | "y2": 1514 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/mi/mi5x_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 300, 3 | "press_coefficient": 1.45, 4 | "piece_base_height_1_2": 25, 5 | "piece_body_width": 80, 6 | "swipe": { 7 | "x1": 560, 8 | "y1": 1550, 9 | "x2": 560, 10 | "y2": 1550 11 | } 12 | } -------------------------------------------------------------------------------- /config/mi/mi6_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 300, 3 | "press_coefficient": 1.45, 4 | "piece_base_height_1_2": 20, 5 | "piece_body_width": 70, 6 | "swipe": { 7 | "x1": 500, 8 | "y1": 1600, 9 | "x2": 500, 10 | "y2": 1600 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/mi/mix2_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 420, 3 | "press_coefficient": 1.480, 4 | "piece_base_height_1_2": 25, 5 | "piece_body_width": 85 6 | } 7 | -------------------------------------------------------------------------------- /config/mi/note2_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 300, 3 | "press_coefficient": 1.47, 4 | "piece_base_height_1_2": 25, 5 | "piece_body_width": 80, 6 | "swipe": { 7 | "x1": 540, 8 | "y1": 1600, 9 | "x2": 540, 10 | "y2": 1600 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/samsung/s7edge_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 384, 3 | "press_coefficient": 1, 4 | "piece_base_height_1_2": 95, 5 | "piece_body_width": 102 6 | } 7 | -------------------------------------------------------------------------------- /config/samsung/s8.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 460, 3 | "press_coefficient": 1.365, 4 | "piece_base_height_1_2": 70, 5 | "piece_body_width": 75 6 | } 7 | -------------------------------------------------------------------------------- /config/samsung/s8在设置里关闭曲面侧屏: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangshub/wechat_jump_game/3cbbb07957901821542fcfc231a09b4f3c9318c9/config/samsung/s8在设置里关闭曲面侧屏 -------------------------------------------------------------------------------- /config/smartisan/pro2_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "under_game_score_y": 411, 3 | "press_coefficient": 1.392, 4 | "piece_base_height_1_2": 20, 5 | "piece_body_width": 70, 6 | "swipe" : { 7 | "x1": 320, 8 | "y1": 410, 9 | "x2": 320, 10 | "y2": 410 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /jump_bot/Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | mkdir -p jumpbot/data 3 | python3 jumpbot/bot.py 4 | -------------------------------------------------------------------------------- /jump_bot/README.rst: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | Wechat Jump Bot (iOS) 3 | ############################################################################## 4 | 5 | ============================================================================== 6 | Features 7 | ============================================================================== 8 | 9 | - Auto Mode: playing the game automatically; 10 | - Manual Mode: playing the game by manual. 11 | 12 | Wechat Jump Game 13 | 14 | .. image:: https://github.com/alpesis-ai/wechat-jumpbot/blob/master/images/auto.png 15 | :height: 1334px 16 | :width: 500px 17 | 18 | 19 | ============================================================================== 20 | How it runs 21 | ============================================================================== 22 | 23 | Prequsites 24 | 25 | - WebDriverAgent 26 | - libimobiledevice 27 | - Python 3 28 | 29 | WebDriverAgent 30 | 31 | :: 32 | 33 | $ git clone https://github.com/facebook/WebDriverAgent && cd WebDriverAgent 34 | $ brew install carthage 35 | $ ./Scripts/bootstrap.sh 36 | # open WebDriverAgent.xcodeproj with Xcode 37 | # Xcode: 38 | # - code sign (general and build_settings): WebDriverAgentLib/WebDriverAgentRunner 39 | # - Product -> Destination -> 40 | # - Product -> Scheme -> WebDriverAgentRunner 41 | # - Product -> Test 42 | 43 | libimobiledevice (iproxy) 44 | 45 | :: 46 | 47 | $ brew install libimobiledevice 48 | $ iproxy 8100 8100 49 | # browse: http://localhost:8100/status 50 | # browse: http://localhost:8100/inspector 51 | 52 | Bot Agent (iOS) 53 | 54 | :: 55 | 56 | $ git clone https://github.com/alpesis-ai/wechat-jumpbot.git 57 | $ cd bot-agent-ios 58 | 59 | $ pip3 install --pre facebook-wda 60 | $ pip3 install -r requirements.txt 61 | 62 | # make run 63 | # - model: [ip, plus, ipx, se] 64 | # - mode: [auto, manual] 65 | # python3 jumpbot/bot.py --model --mode 66 | $ mkdir -p jumpbot/data 67 | # iphone 6/7 68 | $ python3 jumpbot/bot.py --model ip --mode auto 69 | # iphone 6/7 plus 70 | $ python3 jumpbot/bot.py --model plus --mode auto 71 | # iphone X 72 | $ python3 jumpbot/bot.py --model ipx --mode auto 73 | # iphone SE 74 | $ python3 jumpbot/bot.py --model se --mode auto 75 | 76 | 77 | ============================================================================== 78 | Algorithms 79 | ============================================================================== 80 | 81 | Manual Mode: 82 | 83 | - click the piece(x, y) and board(x, y) and get the coordinates correspondingly 84 | - calculating the distance and press time 85 | 86 | :: 87 | 88 | (coord1[0][0] - coord2[0][0])**2 + (coord2[0][1] - coord2[0][1])**2 89 | distance = distance ** 0.5 90 | press_time = distance * settings.TIME_COEFF 91 | 92 | Auto Mode: 93 | 94 | - the main idea same as the manual mode, but detecting the piece and the board automatically 95 | - find coord_y_start_scan 96 | - find piece 97 | - find board 98 | 99 | 100 | ============================================================================== 101 | Developement 102 | ============================================================================== 103 | 104 | :: 105 | 106 | connector ---| 107 | | --> auto / manual --> bot 108 | algos ---| 109 | -------------------------------------------------------------------------------- /jump_bot/jumpbot/algos.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | def get_press_time(piece_x, piece_y, board_x, board_y, time_coeff): 4 | distance = math.sqrt((board_x - piece_x) ** 2 + (board_y - piece_y) ** 2) 5 | press_time = distance * time_coeff / 1000 6 | return press_time 7 | -------------------------------------------------------------------------------- /jump_bot/jumpbot/auto.py: -------------------------------------------------------------------------------- 1 | import time 2 | import math 3 | import random 4 | 5 | from PIL import Image, ImageDraw 6 | 7 | import settings 8 | from connector import Connector 9 | from algos import get_press_time 10 | 11 | 12 | class AutoBot(Connector): 13 | 14 | def __init__(self, params=settings.get_bot_params()): 15 | # init connector 16 | super(AutoBot, self).__init__() 17 | 18 | # init bot 19 | self.status = True 20 | 21 | # init game 22 | self.params = params 23 | self.swipe_x1 = 0; 24 | self.swipe_y1 = 0; 25 | self.swipe_x2 = 0; 26 | self.swipe_y2 = 0; 27 | 28 | def run(self): 29 | steps = 0 30 | 31 | while (self.status): 32 | self.connector_screenshot() 33 | image = Image.open(self.image_dir) 34 | 35 | steps += 1 36 | coord_y_start_scan = self._get_coord_y_start_scan(image) 37 | piece_x, piece_y = self._find_piece(image, coord_y_start_scan) 38 | board_x, board_y = self._find_board(image, piece_x, piece_y) 39 | print("step: ", steps) 40 | print("- image: ", image.size) 41 | print("- coord_y_start_scan: ", coord_y_start_scan) 42 | print("- piece (x, y): ", piece_x, piece_y) 43 | print("- board (x, y): ", board_x, board_y) 44 | if piece_x == 0: 45 | print("Game Over.") 46 | return 47 | 48 | self._set_button_coords(image) 49 | press_time = get_press_time(piece_x, piece_y, board_x, board_y, self.params["TIME_COEFF"]) 50 | print("- press time: ", press_time) 51 | self.connector_taphold(press_time) 52 | time.sleep(random.uniform(1, 1.1)) 53 | 54 | 55 | def _get_coord_y_start_scan(self, image): 56 | width, height = image.size 57 | pixels = image.load() 58 | coord_y_start_scan = 0 59 | 60 | for i in range(self.params["COORD_Y_START_SCAN"], height, 50): 61 | last_pixel = pixels[0, i] 62 | for j in range(1, width): 63 | pixel = pixels[j, i] 64 | 65 | if (pixel[0] != last_pixel[0]) or (pixel[1] != last_pixel[1]) or (pixel[2] != last_pixel[2]): 66 | coord_y_start_scan = i - 50 67 | break; 68 | 69 | if coord_y_start_scan: break; 70 | 71 | return coord_y_start_scan 72 | 73 | def _find_piece(self, image, coord_y_start_scan): 74 | width, height = image.size 75 | pixels = image.load() 76 | 77 | border_x_scan = int(width/8) 78 | piece_x_sum = 0 79 | piece_x_counter = 0 80 | piece_y_max = 0 81 | 82 | for i in range(coord_y_start_scan, int(height * 2 / 3)): 83 | for j in range(border_x_scan, width - border_x_scan): 84 | pixel = pixels[j, i] 85 | if (50 < pixel[0] < 60) and (53 < pixel[1] < 63) and (95 < pixel[2] < 110): 86 | piece_x_sum += j 87 | piece_x_counter += 1 88 | piece_y_max = max(i, piece_y_max) 89 | 90 | if not all((piece_x_sum, piece_x_counter)): return 0, 0 91 | piece_x = piece_x_sum / piece_x_counter 92 | piece_y = piece_y_max - self.params["PIECE_BASE_HEIGHT_HALF"] 93 | return piece_x, piece_y 94 | 95 | 96 | def _find_board(self, image, piece_x, piece_y): 97 | width, height = image.size 98 | pixels = image.load() 99 | 100 | board_x = 0 101 | board_y = 0 102 | 103 | for i in range(int(height/3), int(height * 2/3)): 104 | if board_x or board_y: 105 | break; 106 | 107 | board_x_sum = 0 108 | board_x_counter = 0 109 | last_pixel = pixels[0, i] 110 | for j in range(width): 111 | pixel = pixels[j, i] 112 | if (abs(j - piece_x) < self.params["PIECE_BODY_WIDTH"]): 113 | continue 114 | if abs(pixel[0] - last_pixel[0]) + abs(pixel[1] - last_pixel[1]) + abs(pixel[2] - last_pixel[2]) > 10: 115 | board_x_sum += j 116 | board_x_counter += 1 117 | 118 | if board_x_sum: 119 | board_x = board_x_sum / board_x_counter 120 | 121 | # find the centroid of next board 122 | board_y = piece_y - abs(board_x - piece_x) * math.sqrt(3) / 3 123 | if not all ((board_x, board_y)): 124 | return 0, 0 125 | 126 | return board_x, board_y 127 | 128 | def _set_button_coords(self, image): 129 | width, height = image.size 130 | left = width / 2 131 | top = 1003 * (height / 1280.0) + 10 132 | self.swipe_x1, self.swipe_y1, self.swipe_x2, self.swipe_y2 = left, top, left, top 133 | -------------------------------------------------------------------------------- /jump_bot/jumpbot/bot.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | import settings 4 | from auto import AutoBot 5 | from manual import ManualBot 6 | 7 | 8 | def config(): 9 | parser = argparse.ArgumentParser(description='Wechat Jump Bot') 10 | parser.add_argument('-ml', '--model', 11 | type=str, 12 | help='device model = [ip, plus, ipx, se]', 13 | required=True) 14 | 15 | parser.add_argument('-me', '--mode', 16 | type=str, 17 | help='mode = [auto, manual]', 18 | required=True) 19 | 20 | return parser.parse_args() 21 | 22 | 23 | def jumpbot(parser): 24 | 25 | if parser.mode == "manual": 26 | bot = ManualBot(params=settings.get_bot_params(parser.model)) 27 | bot.run() 28 | 29 | elif parser.mode == "auto": 30 | bot = AutoBot(params=settings.get_bot_params(parser.model)) 31 | bot.run() 32 | 33 | else: 34 | print("ParamError: MODE should be ['auto', 'manual'].") 35 | 36 | 37 | if __name__ == '__main__': 38 | 39 | parser = config() 40 | jumpbot(parser) 41 | -------------------------------------------------------------------------------- /jump_bot/jumpbot/connector.py: -------------------------------------------------------------------------------- 1 | import wda 2 | 3 | import settings 4 | 5 | 6 | class Connector: 7 | 8 | 9 | def __init__(self, image_dir=settings.IMAGE_DIR): 10 | self.image_dir = image_dir 11 | 12 | self.client = wda.Client() 13 | self.session = self.client.session() 14 | 15 | 16 | def connector_screenshot(self): 17 | self.client.screenshot(self.image_dir) 18 | 19 | 20 | def connector_taphold(self, value): 21 | self.session.tap_hold(200, 200, value) 22 | -------------------------------------------------------------------------------- /jump_bot/jumpbot/manual.py: -------------------------------------------------------------------------------- 1 | import time 2 | import math 3 | 4 | import numpy as np 5 | from PIL import Image 6 | import matplotlib.pyplot as plt 7 | import matplotlib.animation as animation 8 | 9 | import settings 10 | from connector import Connector 11 | from algos import get_press_time 12 | 13 | 14 | class ManualBot(Connector): 15 | 16 | def __init__(self, params=settings.get_bot_params()): 17 | # init connector 18 | super(ManualBot, self).__init__() 19 | 20 | # init figure 21 | self.figure = plt.figure() 22 | 23 | # actions 24 | self.steps = 0 25 | self.params = params 26 | self.coords = [] 27 | self.ix = [0, 0] 28 | self.iy = [0, 0] 29 | self.click_counter = 0 30 | self.status = True 31 | 32 | 33 | def run(self): 34 | self.connector_screenshot() 35 | self.image = plt.imshow(self._read_image(), animated=True) 36 | self.action() 37 | 38 | def action(self): 39 | self.figure.canvas.mpl_connect('button_press_event', self._onclick) 40 | ani = animation.FuncAnimation(self.figure, self._update_figure, interval=50, blit=True) 41 | plt.show() 42 | 43 | 44 | def _onclick(self, event): 45 | coord = [] 46 | self.ix, self.iy = event.xdata, event.ydata 47 | coord.append((self.ix, self.iy)) 48 | print("coordinate = ", coord) 49 | self.coords.append(coord) 50 | self.click_counter += 1 51 | 52 | if self.click_counter > 1: 53 | self.click_counter = 0 54 | # next screen 55 | coord1 = self.coords.pop() 56 | coord2 = self.coords.pop() 57 | press_time = get_press_time(coord1[0][0], coord1[0][1], 58 | coord2[0][0], coord2[0][1], 59 | self.params["TIME_COEFF"]) 60 | self.steps += 1 61 | print("Step: ", self.steps) 62 | print("- coord1: ", coord1) 63 | print("- coord2: ", coord2) 64 | print("- press_time: ", press_time) 65 | self.connector_taphold(press_time) 66 | self.status = True 67 | 68 | 69 | def _update_figure(self, *args): 70 | if self.status: 71 | time.sleep(1) 72 | self.connector_screenshot() 73 | self.image.set_array(self._read_image()) 74 | self.status = False 75 | return self.image, 76 | 77 | 78 | def _read_image(self): 79 | return np.array(Image.open(self.image_dir)) 80 | -------------------------------------------------------------------------------- /jump_bot/jumpbot/settings.py: -------------------------------------------------------------------------------- 1 | # Wechat Jump Bot (iOS) 2 | # ---------------------------------------------------------------------------- 3 | 4 | import os 5 | 6 | CURRENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 7 | PROJECT_ROOT = os.path.dirname(CURRENT_DIR) 8 | PROJECT_DIR = "jumpbot/" 9 | 10 | # ---------------------------------------------------------------------------- 11 | # Screenshot 12 | 13 | DATA_DIR = "data/" 14 | IMAGE = "screen.png" 15 | IMAGE_DIR = PROJECT_DIR + DATA_DIR + IMAGE 16 | 17 | # ---------------------------------------------------------------------------- 18 | 19 | # mode: ['auto', 'manual'] 20 | MODE = "manual" 21 | 22 | # ---------------------------------------------------------------------------- 23 | # Params 24 | 25 | def get_bot_params(model="ip"): 26 | 27 | bot_params = { 28 | "TIME_COEFF": 2., 29 | "COORD_Y_START_SCAN": 200, 30 | "PIECE_BASE_HEIGHT_HALF": 13, 31 | "PIECE_BODY_WIDTH": 49, 32 | "SWIPE_X1": 375, 33 | "SWIPE_Y1": 1055, 34 | "SWIPE_X2": 375, 35 | "SWIPE_Y2": 1055 36 | } 37 | 38 | if model == "ip": 39 | bot_params["TIME_COEFF"] = 2. 40 | bot_params["COORD_Y_START_SCAN"] = 200 41 | bot_params["PIECE_BASE_HEIGHT_HALF"] = 13 42 | bot_params["PIECE_BODY_WIDTH"] = 49 43 | bot_params["SWIPE_X1"] = 375 44 | bot_params["SWIPE_Y1"] = 1055 45 | bot_params["SWIPE_X2"] = 375 46 | bot_params["SWIPE_Y2"] = 1055 47 | 48 | elif model == "plus": 49 | bot_params["TIME_COEFF"] = 1.2 50 | bot_params["COORD_Y_START_SCAN"] = 300 51 | bot_params["PIECE_BASE_HEIGHT_HALF"] = 20 52 | bot_params["PIECE_BODY_WIDTH"] = 70 53 | bot_params["SWIPE_X1"] = 320 54 | bot_params["SWIPE_Y1"] = 410 55 | bot_params["SWIPE_X2"] = 320 56 | bot_params["SWIPE_Y2"] = 410 57 | 58 | elif model == "ipx": 59 | bot_params["TIME_COEFF"] = 1.31 60 | bot_params["COORD_Y_START_SCAN"] = 170 61 | bot_params["PIECE_BASE_HEIGHT_HALF"] = 23 62 | bot_params["PIECE_BODY_WIDTH"] = 70 63 | bot_params["SWIPE_X1"] = 320 64 | bot_params["SWIPE_Y1"] = 410 65 | bot_params["SWIPE_X2"] = 320 66 | bot_params["SWIPE_Y2"] = 410 67 | 68 | elif model == "se": 69 | bot_params["TIME_COEFF"] = 2.3 70 | bot_params["COORD_Y_START_SCAN"] = 190 71 | bot_params["PIECE_BASE_HEIGHT_HALF"] = 12 72 | bot_params["PIECE_BODY_WIDTH"] = 50 73 | bot_params["SWIPE_X1"] = 375 74 | bot_params["SWIPE_Y1"] = 1055 75 | bot_params["SWIPE_X2"] = 375 76 | bot_params["SWIPE_Y2"] = 1055 77 | 78 | else: 79 | print("ParamError: Unknown model type, model should be [ip, plus, ipx, se]") 80 | 81 | return bot_params 82 | -------------------------------------------------------------------------------- /jump_bot/requirements.txt: -------------------------------------------------------------------------------- 1 | backports.functools-lru-cache==1.4 2 | cycler==0.10.0 3 | matplotlib==2.1.1 4 | numpy==1.13.3 5 | olefile==0.44 6 | opencv-python==3.4.0.12 7 | Pillow==4.3.0 8 | pyparsing==2.2.0 9 | python-dateutil==2.6.1 10 | pytz==2017.3 11 | six==1.11.0 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | backports.functools-lru-cache==1.4 2 | cycler==0.10.0 3 | matplotlib==2.1.1 4 | numpy==1.13.3 5 | olefile==0.44 6 | opencv-python==3.4.0.12 7 | Pillow==4.3.0 8 | pyparsing==2.2.0 9 | python-dateutil==2.6.1 10 | pytz==2017.3 11 | six==1.11.0 12 | tensorflow==1.4.0 13 | pandas==0.22.0 14 | scipy==1.0.0 15 | scikit_learn==0.19.1 16 | -------------------------------------------------------------------------------- /resource/image/character.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangshub/wechat_jump_game/3cbbb07957901821542fcfc231a09b4f3c9318c9/resource/image/character.png -------------------------------------------------------------------------------- /resource/image/jump.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangshub/wechat_jump_game/3cbbb07957901821542fcfc231a09b4f3c9318c9/resource/image/jump.gif -------------------------------------------------------------------------------- /resource/image/qrcode_for_gh_3586401957c4_258.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangshub/wechat_jump_game/3cbbb07957901821542fcfc231a09b4f3c9318c9/resource/image/qrcode_for_gh_3586401957c4_258.jpg -------------------------------------------------------------------------------- /resource/model/checkpoint: -------------------------------------------------------------------------------- 1 | model_checkpoint_path: "model.ckpt" 2 | all_model_checkpoint_paths: "model.ckpt" 3 | -------------------------------------------------------------------------------- /resource/model/model.ckpt.data-00000-of-00001: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangshub/wechat_jump_game/3cbbb07957901821542fcfc231a09b4f3c9318c9/resource/model/model.ckpt.data-00000-of-00001 -------------------------------------------------------------------------------- /resource/model/model.ckpt.index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangshub/wechat_jump_game/3cbbb07957901821542fcfc231a09b4f3c9318c9/resource/model/model.ckpt.index -------------------------------------------------------------------------------- /resource/model/model.ckpt.meta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangshub/wechat_jump_game/3cbbb07957901821542fcfc231a09b4f3c9318c9/resource/model/model.ckpt.meta -------------------------------------------------------------------------------- /wechat_jump.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division 3 | import os 4 | import time 5 | import datetime 6 | import matplotlib.pyplot as plt 7 | import matplotlib.animation as animation 8 | import cv2 9 | 10 | VERSION = "1.1.4" 11 | scale = 0.25 12 | 13 | template = cv2.imread('./resource/image/character.png') 14 | template = cv2.resize(template, (0, 0), fx=scale, fy=scale) 15 | template_size = template.shape[:2] 16 | 17 | 18 | def search(img): 19 | result = cv2.matchTemplate(img, template, cv2.TM_SQDIFF) 20 | min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) 21 | 22 | cv2.rectangle( 23 | img, 24 | (min_loc[0], min_loc[1]), 25 | (min_loc[0] + template_size[1], min_loc[1] + template_size[0]), 26 | (255, 0, 0), 27 | 4) 28 | return img, min_loc[0] + template_size[1] / 2, min_loc[1] + template_size[0] 29 | 30 | 31 | def pull_screenshot(): 32 | filename = datetime.datetime.now().strftime("%H%M%S") + '.png' 33 | os.system('mv autojump.png {}'.format(filename)) 34 | os.system('adb shell screencap -p /sdcard/autojump.png') 35 | os.system('adb pull /sdcard/autojump.png ./autojump.png') 36 | 37 | 38 | def jump(distance): 39 | press_time = distance * 1.35 40 | press_time = int(press_time) 41 | cmd = 'adb shell input swipe 320 410 320 410 ' + str(press_time) 42 | print(cmd) 43 | os.system(cmd) 44 | 45 | 46 | def update_data(): 47 | global src_x, src_y 48 | 49 | img = cv2.imread('./autojump.png') 50 | img = cv2.resize(img, (0, 0), fx=scale, fy=scale) 51 | img, src_x, src_y = search(img) 52 | return img 53 | 54 | 55 | fig = plt.figure() 56 | pull_screenshot() 57 | img = update_data() 58 | im = plt.imshow(img, animated=True) 59 | 60 | update = True 61 | 62 | 63 | def updatefig(*args): 64 | global update 65 | 66 | if update: 67 | time.sleep(1) 68 | pull_screenshot() 69 | im.set_array(update_data()) 70 | update = False 71 | return im, 72 | 73 | 74 | def on_click(event): 75 | global update 76 | global src_x, src_y 77 | 78 | dst_x, dst_y = event.xdata, event.ydata 79 | 80 | distance = (dst_x - src_x)**2 + (dst_y - src_y)**2 81 | distance = (distance ** 0.5) / scale 82 | print('distance = ', distance) 83 | jump(distance) 84 | update = True 85 | 86 | 87 | fig.canvas.mpl_connect('button_press_event', on_click) 88 | ani = animation.FuncAnimation(fig, updatefig, interval=5, blit=True) 89 | plt.show() 90 | -------------------------------------------------------------------------------- /wechat_jump_auto.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | === 思路 === 5 | 核心:每次落稳之后截图,根据截图算出棋子的坐标和下一个块顶面的中点坐标, 6 | 根据两个点的距离乘以一个时间系数获得长按的时间 7 | 识别棋子:靠棋子的颜色来识别位置,通过截图发现最下面一行大概是一条 8 | 直线,就从上往下一行一行遍历,比较颜色(颜色用了一个区间来比较) 9 | 找到最下面的那一行的所有点,然后求个中点,求好之后再让 Y 轴坐标 10 | 减小棋子底盘的一半高度从而得到中心点的坐标 11 | 识别棋盘:靠底色和方块的色差来做,从分数之下的位置开始,一行一行扫描, 12 | 由于圆形的块最顶上是一条线,方形的上面大概是一个点,所以就 13 | 用类似识别棋子的做法多识别了几个点求中点,这时候得到了块中点的 X 14 | 轴坐标,这时候假设现在棋子在当前块的中心,根据一个通过截图获取的 15 | 固定的角度来推出中点的 Y 坐标 16 | 最后:根据两点的坐标算距离乘以系数来获取长按时间(似乎可以直接用 X 轴距离) 17 | """ 18 | 19 | import math 20 | import re 21 | import random 22 | import sys 23 | import time 24 | from PIL import Image 25 | from six.moves import input 26 | 27 | if sys.version_info.major != 3: 28 | print('请使用Python3') 29 | exit(1) 30 | try: 31 | from common import debug, config, screenshot, UnicodeStreamFilter 32 | from common.auto_adb import auto_adb 33 | except Exception as ex: 34 | print(ex) 35 | print('请将脚本放在项目根目录中运行') 36 | print('请检查项目根目录中的 common 文件夹是否存在') 37 | exit(1) 38 | adb = auto_adb() 39 | VERSION = "1.1.4" 40 | 41 | # DEBUG 开关,需要调试的时候请改为 True,不需要调试的时候为 False 42 | DEBUG_SWITCH = False 43 | adb.test_device() 44 | # Magic Number,不设置可能无法正常执行,请根据具体截图从上到下按需 45 | # 设置,设置保存在 config 文件夹中 46 | config = config.open_accordant_config() 47 | under_game_score_y = config['under_game_score_y'] 48 | # 长按的时间系数,请自己根据实际情况调节 49 | press_coefficient = config['press_coefficient'] 50 | # 二分之一的棋子底座高度,可能要调节 51 | piece_base_height_1_2 = config['piece_base_height_1_2'] 52 | # 棋子的宽度,比截图中量到的稍微大一点比较安全,可能要调节 53 | piece_body_width = config['piece_body_width'] 54 | # 图形中圆球的直径,可以利用系统自带画图工具,用直线测量像素,如果可以实现自动识别圆球直径,那么此处将可实现全自动。 55 | head_diameter = config.get('head_diameter') 56 | if head_diameter == None: 57 | density_str = adb.test_density() 58 | matches = re.search(r'\d+', density_str) 59 | density_val = int(matches.group(0)) 60 | head_diameter = density_val / 8 61 | 62 | 63 | def set_button_position(im): 64 | """ 65 | 将 swipe 设置为 `再来一局` 按钮的位置 66 | """ 67 | global swipe_x1, swipe_y1, swipe_x2, swipe_y2 68 | w, h = im.size 69 | left = int(w / 2) 70 | top = int(1584 * (h / 1920.0)) 71 | left = int(random.uniform(left - 200, left + 200)) 72 | top = int(random.uniform(top - 200, top + 200)) # 随机防 ban 73 | after_top = int(random.uniform(top - 200, top + 200)) 74 | after_left = int(random.uniform(left - 200, left + 200)) 75 | swipe_x1, swipe_y1, swipe_x2, swipe_y2 = left, top, after_left, after_top 76 | 77 | 78 | def jump(distance, delta_piece_y): 79 | """ 80 | 跳跃一定的距离 81 | """ 82 | # 计算程序长度与截图测得的距离的比例 83 | scale = 0.945 * 2 / head_diameter 84 | actual_distance = distance * scale * (math.sqrt(6) / 2) 85 | press_time = (-945 + math.sqrt(945 ** 2 + 4 * 105 * 86 | 36 * actual_distance)) / (2 * 105) * 1000 87 | press_time *= press_coefficient 88 | press_time = max(press_time, 200) # 设置 200ms 是最小的按压时间 89 | press_time = int(press_time) 90 | 91 | cmd = 'shell input swipe {x1} {y1} {x2} {y2} {duration}'.format( 92 | x1=swipe_x1, 93 | y1=swipe_y1, 94 | x2=swipe_x2, 95 | y2=swipe_y2, 96 | duration=press_time + delta_piece_y 97 | ) 98 | print(cmd) 99 | adb.run(cmd) 100 | return press_time 101 | 102 | 103 | def find_piece_and_board(im): 104 | """ 105 | 寻找关键坐标 106 | """ 107 | w, h = im.size 108 | points = [] # 所有满足色素的点集合 109 | piece_y_max = 0 110 | board_x = 0 111 | board_y = 0 112 | scan_x_border = int(w / 8) # 扫描棋子时的左右边界 113 | scan_start_y = 0 # 扫描的起始 y 坐标 114 | im_pixel = im.load() 115 | # 以 50px 步长,尝试探测 scan_start_y 116 | for i in range(int(h / 3), int(h * 2 / 3), 50): 117 | last_pixel = im_pixel[0, i] 118 | for j in range(1, w): 119 | pixel = im_pixel[j, i] 120 | # 不是纯色的线,则记录 scan_start_y 的值,准备跳出循环 121 | if pixel != last_pixel: 122 | scan_start_y = i - 50 123 | break 124 | if scan_start_y: 125 | break 126 | print('start scan Y axis: {}'.format(scan_start_y)) 127 | 128 | # 从 scan_start_y 开始往下扫描,棋子应位于屏幕上半部分,这里暂定不超过 2/3 129 | for i in range(scan_start_y, int(h * 2 / 3)): 130 | # 横坐标方面也减少了一部分扫描开销 131 | for j in range(scan_x_border, w - scan_x_border): 132 | pixel = im_pixel[j, i] 133 | # 根据棋子的最低行的颜色判断,找最后一行那些点的平均值,这个颜 134 | # 色这样应该 OK,暂时不提出来 135 | if (50 < pixel[0] < 60) \ 136 | and (53 < pixel[1] < 63) \ 137 | and (95 < pixel[2] < 110): 138 | points.append((j, i)) 139 | piece_y_max = max(i, piece_y_max) 140 | 141 | bottom_x = [x for x, y in points if y == piece_y_max] # 所有最底层的点的横坐标 142 | if not bottom_x: 143 | return 0, 0, 0, 0, 0 144 | 145 | piece_x = int(sum(bottom_x) / len(bottom_x)) # 中间值 146 | piece_y = piece_y_max - piece_base_height_1_2 # 上移棋子底盘高度的一半 147 | 148 | # 限制棋盘扫描的横坐标,避免音符 bug 149 | if piece_x < w / 2: 150 | board_x_start = piece_x 151 | board_x_end = w 152 | else: 153 | board_x_start = 0 154 | board_x_end = piece_x 155 | 156 | for i in range(int(h / 3), int(h * 2 / 3)): 157 | last_pixel = im_pixel[0, i] 158 | if board_x or board_y: 159 | break 160 | board_x_sum = 0 161 | board_x_c = 0 162 | 163 | for j in range(int(board_x_start), int(board_x_end)): 164 | pixel = im_pixel[j, i] 165 | # 修掉脑袋比下一个小格子还高的情况的 bug 166 | if abs(j - piece_x) < piece_body_width: 167 | continue 168 | 169 | # 检查Y轴下面5个像素, 和背景色相同, 那么是干扰 170 | ver_pixel = im_pixel[j, i + 5] 171 | if abs(pixel[0] - last_pixel[0]) \ 172 | + abs(pixel[1] - last_pixel[1]) \ 173 | + abs(pixel[2] - last_pixel[2]) > 10 \ 174 | and abs(ver_pixel[0] - last_pixel[0]) \ 175 | + abs(ver_pixel[1] - last_pixel[1]) \ 176 | + abs(ver_pixel[2] - last_pixel[2]) > 10: 177 | board_x_sum += j 178 | board_x_c += 1 179 | if board_x_sum: 180 | board_x = board_x_sum / board_x_c 181 | last_pixel = im_pixel[board_x, i] 182 | 183 | # 首先找到游戏的对称中心,由对称中心做辅助线与x=board_x直线的交点即为棋盘的中心位置 184 | # 有了对称中心,可以知道棋子在棋盘上面的相对位置(偏高或偏低,偏高的话测量值比实际值大, 185 | # 偏低相反。最后通过delta_piece_y来对跳跃时间进行微调 186 | center_x = w / 2 + (24 / 1080) * w 187 | center_y = h / 2 + (17 / 1920) * h 188 | if piece_x > center_x: 189 | board_y = round((25.5 / 43.5) * (board_x - center_x) + center_y) 190 | delta_piece_y = piece_y - round((25.5 / 43.5) * (piece_x - center_x) + center_y) 191 | else: 192 | board_y = round(-(25.5 / 43.5) * (board_x - center_x) + center_y) 193 | delta_piece_y = piece_y - round(-(25.5 / 43.5) * (piece_x - center_x) + center_y) 194 | 195 | if not all((board_x, board_y)): 196 | return 0, 0, 0, 0, 0 197 | return piece_x, piece_y, board_x, board_y, delta_piece_y 198 | 199 | 200 | def yes_or_no(): 201 | """ 202 | 检查是否已经为启动程序做好了准备 203 | """ 204 | while True: 205 | yes_or_no = str(input('请确保手机打开了 ADB 并连接了电脑,' 206 | '然后打开跳一跳并【开始游戏】后再用本程序,确定开始?[y/n]:')) 207 | if yes_or_no == 'y': 208 | break 209 | elif yes_or_no == 'n': 210 | print('谢谢使用', end='') 211 | exit(0) 212 | else: 213 | print('请重新输入') 214 | 215 | 216 | def main(): 217 | """ 218 | 主函数 219 | """ 220 | print('程序版本号:{}'.format(VERSION)) 221 | print('激活窗口并按 CONTROL + C 组合键退出') 222 | debug.dump_device_info() 223 | screenshot.check_screenshot() 224 | 225 | i, next_rest, next_rest_time = (0, random.randrange(3, 10), 226 | random.randrange(5, 10)) 227 | while True: 228 | im = screenshot.pull_screenshot() 229 | # 获取棋子和 board 的位置 230 | piece_x, piece_y, board_x, board_y, delta_piece_y = find_piece_and_board(im) 231 | ts = int(time.time()) 232 | print(ts, piece_x, piece_y, board_x, board_y) 233 | set_button_position(im) 234 | jump(math.sqrt((board_x - piece_x) ** 2 + (board_y - piece_y) ** 2), delta_piece_y) 235 | if DEBUG_SWITCH: 236 | debug.save_debug_screenshot(ts, im, piece_x, 237 | piece_y, board_x, board_y) 238 | debug.backup_screenshot(ts) 239 | im.close() 240 | i += 1 241 | if i == next_rest: 242 | print('已经连续打了 {} 下,休息 {}秒'.format(i, next_rest_time)) 243 | for j in range(next_rest_time): 244 | sys.stdout.write('\r程序将在 {}秒 后继续'.format(next_rest_time - j)) 245 | sys.stdout.flush() 246 | time.sleep(1) 247 | print('\n继续') 248 | i, next_rest, next_rest_time = (0, random.randrange(30, 100), 249 | random.randrange(10, 60)) 250 | # 为了保证截图的时候应落稳了,多延迟一会儿,随机值防 ban 251 | time.sleep(random.uniform(1.2, 1.4)) 252 | 253 | 254 | if __name__ == '__main__': 255 | try: 256 | yes_or_no() 257 | main() 258 | except KeyboardInterrupt: 259 | adb.run('kill-server') 260 | print('\n谢谢使用', end='') 261 | exit(0) 262 | -------------------------------------------------------------------------------- /wechat_jump_auto_ai.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | ''' 3 | # === 思路 === 4 | # 核心:每次落稳之后截图,根据截图算出棋子的坐标和下一个块顶面的中点坐标, 5 | # 根据两个点的距离乘以一个时间系数获得长按的时间 6 | # 识别棋子:靠棋子的颜色来识别位置,通过截图发现最下面一行大概是一条直线,就从上往下一行一行遍历, 7 | # 比较颜色(颜色用了一个区间来比较)找到最下面的那一行的所有点,然后求个中点, 8 | # 求好之后再让 Y 轴坐标减小棋子底盘的一半高度从而得到中心点的坐标 9 | # 识别棋盘:靠底色和方块的色差来做,从分数之下的位置开始,一行一行扫描,由于圆形的块最顶上是一条线, 10 | # 方形的上面大概是一个点,所以就用类似识别棋子的做法多识别了几个点求中点, 11 | # 这时候得到了块中点的 X 轴坐标,这时候假设现在棋子在当前块的中心, 12 | # 根据一个通过截图获取的固定的角度来推出中点的 Y 坐标 13 | # 最后:根据两点的坐标算距离乘以系数来获取长按时间(似乎可以直接用 X 轴距离) 14 | ''' 15 | import os 16 | import sys 17 | import subprocess 18 | import time 19 | import math 20 | import pandas 21 | from PIL import Image 22 | import random 23 | from six.moves import input 24 | 25 | try: 26 | from common import ai, debug, config, UnicodeStreamFilter 27 | from common.auto_adb import auto_adb 28 | except Exception as ex: 29 | print(ex) 30 | print('请将脚本放在项目根目录中运行') 31 | print('请检查项目根目录中的 common 文件夹是否存在') 32 | exit(1) 33 | adb = auto_adb() 34 | VERSION = "1.1.4" 35 | 36 | debug_switch = True # debug 开关,需要调试的时候请改为:True 37 | config = config.open_accordant_config() 38 | 39 | # Magic Number,不设置可能无法正常执行,请根据具体截图从上到下按需设置,设置保存在 config 文件夹中 40 | under_game_score_y = config['under_game_score_y'] 41 | press_coefficient = config['press_coefficient'] # 长按的时间系数,请自己根据实际情况调节 42 | piece_base_height_1_2 = config['piece_base_height_1_2'] # 二分之一的棋子底座高度,可能要调节 43 | piece_body_width = config['piece_body_width'] # 棋子的宽度,比截图中量到的稍微大一点比较安全,可能要调节 44 | 45 | screenshot_way = 2 46 | 47 | 48 | def pull_screenshot(): 49 | process = subprocess.Popen('adb shell screencap -p', shell=True, stdout=subprocess.PIPE) 50 | screenshot = process.stdout.read() 51 | if sys.platform == 'win32': 52 | screenshot = screenshot.replace(b'\r\n', b'\n') 53 | f = open('autojump.png', 'wb') 54 | f.write(screenshot) 55 | f.close() 56 | 57 | 58 | def pull_screenshot_temp(): 59 | process = subprocess.Popen('adb shell screencap -p', shell=True, stdout=subprocess.PIPE) 60 | screenshot = process.stdout.read() 61 | if sys.platform == 'win32': 62 | screenshot = screenshot.replace(b'\r\n', b'\n') 63 | f = open('autojump_temp.png', 'wb') 64 | f.write(screenshot) 65 | f.close() 66 | 67 | 68 | def set_button_position(im): 69 | """ 70 | 将 swipe 设置为 `再来一局` 按钮的位置 71 | """ 72 | global swipe_x1, swipe_y1, swipe_x2, swipe_y2 73 | w, h = im.size 74 | left = int(w / 2) 75 | top = int(1584 * (h / 1920.0)) 76 | left = int(random.uniform(left - 100, left + 100)) 77 | top = int(random.uniform(top - 100, top + 100)) # 随机防 ban 78 | after_top = int(random.uniform(top - 100, top + 100)) 79 | after_left = int(random.uniform(left - 100, left + 100)) 80 | swipe_x1, swipe_y1, swipe_x2, swipe_y2 = left, top, after_left, after_top 81 | 82 | 83 | def jump(distance): 84 | ''' 85 | 跳跃一定的距离 86 | ''' 87 | if ai.get_result_len() >= 10: # 需采集10条样本以上 88 | k, b, v = ai.computing_k_b_v(distance) 89 | press_time = distance * k[0] + b 90 | print('Y = {k} * X + {b}'.format(k=k[0], b=b)) 91 | 92 | else: 93 | press_time = distance * press_coefficient 94 | press_time = max(press_time, 200) # 设置 200ms 是最小的按压时间 95 | 96 | press_time = int(press_time) 97 | cmd = 'shell input swipe {x1} {y1} {x2} {y2} {duration}'.format( 98 | x1=swipe_x1, 99 | y1=swipe_y1, 100 | x2=swipe_x2, 101 | y2=swipe_y2, 102 | duration=press_time 103 | ) 104 | print('{}'.format(cmd)) 105 | adb.run(cmd) 106 | return press_time 107 | 108 | 109 | # 转换色彩模式hsv2rgb 110 | def hsv2rgb(h, s, v): 111 | h = float(h) 112 | s = float(s) 113 | v = float(v) 114 | h60 = h / 60.0 115 | h60f = math.floor(h60) 116 | hi = int(h60f) % 6 117 | f = h60 - h60f 118 | p = v * (1 - s) 119 | q = v * (1 - f * s) 120 | t = v * (1 - (1 - f) * s) 121 | r, g, b = 0, 0, 0 122 | if hi == 0: 123 | r, g, b = v, t, p 124 | elif hi == 1: 125 | r, g, b = q, v, p 126 | elif hi == 2: 127 | r, g, b = p, v, t 128 | elif hi == 3: 129 | r, g, b = p, q, v 130 | elif hi == 4: 131 | r, g, b = t, p, v 132 | elif hi == 5: 133 | r, g, b = v, p, q 134 | r, g, b = int(r * 255), int(g * 255), int(b * 255) 135 | return r, g, b 136 | 137 | 138 | # 转换色彩模式rgb2hsv 139 | def rgb2hsv(r, g, b): 140 | r, g, b = r / 255.0, g / 255.0, b / 255.0 141 | mx = max(r, g, b) 142 | mn = min(r, g, b) 143 | df = mx - mn 144 | if mx == mn: 145 | h = 0 146 | elif mx == r: 147 | h = (60 * ((g - b) / df) + 360) % 360 148 | elif mx == g: 149 | h = (60 * ((b - r) / df) + 120) % 360 150 | elif mx == b: 151 | h = (60 * ((r - g) / df) + 240) % 360 152 | if mx == 0: 153 | s = 0 154 | else: 155 | s = df / mx 156 | v = mx 157 | return h, s, v 158 | 159 | 160 | def find_piece(im): 161 | ''' 162 | 寻找关键坐标 163 | ''' 164 | w, h = im.size 165 | 166 | piece_x_sum = 0 167 | piece_x_c = 0 168 | piece_y_max = 0 169 | scan_x_border = int(w / 8) # 扫描棋子时的左右边界 170 | scan_start_y = 0 # 扫描的起始 y 坐标 171 | im_pixel = im.load() 172 | # 以 50px 步长,尝试探测 scan_start_y 173 | for i in range(int(h / 3), int(h * 2 / 3), 50): 174 | last_pixel = im_pixel[0, i] 175 | for j in range(1, w): 176 | pixel = im_pixel[j, i] 177 | # 不是纯色的线,则记录 scan_start_y 的值,准备跳出循环 178 | if pixel[0] != last_pixel[0] or pixel[1] != last_pixel[1] or pixel[2] != last_pixel[2]: 179 | scan_start_y = i - 50 180 | break 181 | if scan_start_y: 182 | break 183 | # print('scan_start_y: {}'.format(scan_start_y)) 184 | 185 | # 从 scan_start_y 开始往下扫描,棋子应位于屏幕上半部分,这里暂定不超过 2/3 186 | for i in range(scan_start_y, int(h * 2 / 3)): 187 | for j in range(scan_x_border, w - scan_x_border): # 横坐标方面也减少了一部分扫描开销 188 | pixel = im_pixel[j, i] 189 | # 根据棋子的最低行的颜色判断,找最后一行那些点的平均值,这个颜色这样应该 OK,暂时不提出来 190 | if (50 < pixel[0] < 60) and (53 < pixel[1] < 63) and (95 < pixel[2] < 110): 191 | piece_x_sum += j 192 | piece_x_c += 1 193 | piece_y_max = max(i, piece_y_max) 194 | 195 | if not all((piece_x_sum, piece_x_c)): 196 | return 0, 0, 197 | piece_x = int(piece_x_sum / piece_x_c) 198 | piece_y = piece_y_max - piece_base_height_1_2 # 上移棋子底盘高度的一半 199 | 200 | return piece_x, piece_y 201 | 202 | 203 | def find_piece_and_board(im): 204 | w, h = im.size 205 | 206 | piece_x_sum = 0 207 | piece_x_c = 0 208 | piece_y_max = 0 209 | board_x = 0 210 | board_y = 0 211 | 212 | left_value = 0 213 | left_count = 0 214 | right_value = 0 215 | right_count = 0 216 | from_left_find_board_y = 0 217 | from_right_find_board_y = 0 218 | 219 | scan_x_border = int(w / 8) # 扫描棋子时的左右边界 220 | scan_start_y = 0 # 扫描的起始y坐标 221 | im_pixel = im.load() 222 | # 以50px步长,尝试探测scan_start_y 223 | for i in range(int(h / 3), int(h * 2 / 3), 50): 224 | last_pixel = im_pixel[0, i] 225 | for j in range(1, w): 226 | pixel = im_pixel[j, i] 227 | # 不是纯色的线,则记录scan_start_y的值,准备跳出循环 228 | if pixel[0] != last_pixel[0] or pixel[1] != last_pixel[1] or pixel[2] != last_pixel[2]: 229 | scan_start_y = i - 50 230 | break 231 | if scan_start_y: 232 | break 233 | # print('scan_start_y: ', scan_start_y) 234 | 235 | # 从scan_start_y开始往下扫描,棋子应位于屏幕上半部分,这里暂定不超过2/3 236 | for i in range(scan_start_y, int(h * 2 / 3)): 237 | for j in range(scan_x_border, w - scan_x_border): # 横坐标方面也减少了一部分扫描开销 238 | pixel = im_pixel[j, i] 239 | # 根据棋子的最低行的颜色判断,找最后一行那些点的平均值,这个颜色这样应该 OK,暂时不提出来 240 | if (50 < pixel[0] < 60) and (53 < pixel[1] < 63) and (95 < pixel[2] < 110): 241 | piece_x_sum += j 242 | piece_x_c += 1 243 | piece_y_max = max(i, piece_y_max) 244 | 245 | if not all((piece_x_sum, piece_x_c)): 246 | return 0, 0, 0, 0 247 | piece_x = piece_x_sum / piece_x_c 248 | piece_y = piece_y_max - piece_base_height_1_2 # 上移棋子底盘高度的一半 249 | 250 | for i in range(int(h / 3), int(h * 2 / 3)): 251 | 252 | last_pixel = im_pixel[0, i] 253 | # 计算阴影的RGB值,通过photoshop观察,阴影部分其实就是背景色的明度V 乘以0.7的样子 254 | h, s, v = rgb2hsv(last_pixel[0], last_pixel[1], last_pixel[2]) 255 | r, g, b = hsv2rgb(h, s, v * 0.7) 256 | 257 | if from_left_find_board_y and from_right_find_board_y: 258 | break 259 | 260 | if not board_x: 261 | board_x_sum = 0 262 | board_x_c = 0 263 | 264 | for j in range(w): 265 | pixel = im_pixel[j, i] 266 | # 修掉脑袋比下一个小格子还高的情况的 bug 267 | if abs(j - piece_x) < piece_body_width: 268 | continue 269 | 270 | # 修掉圆顶的时候一条线导致的小 bug,这个颜色判断应该 OK,暂时不提出来 271 | if abs(pixel[0] - last_pixel[0]) + abs(pixel[1] - last_pixel[1]) + abs(pixel[2] - last_pixel[2]) > 10: 272 | board_x_sum += j 273 | board_x_c += 1 274 | if board_x_sum: 275 | board_x = board_x_sum / board_x_c 276 | else: 277 | # 继续往下查找,从左到右扫描,找到第一个与背景颜色不同的像素点,记录位置 278 | # 当有连续3个相同的记录时,表示发现了一条直线 279 | # 这条直线即为目标board的左边缘 280 | # 然后当前的 y 值减 3 获得左边缘的第一个像素 281 | # 就是顶部的左边顶点 282 | for j in range(w): 283 | pixel = im_pixel[j, i] 284 | # 修掉脑袋比下一个小格子还高的情况的 bug 285 | if abs(j - piece_x) < piece_body_width: 286 | continue 287 | if (abs(pixel[0] - last_pixel[0]) + abs(pixel[1] - last_pixel[1]) + abs(pixel[2] - last_pixel[2]) 288 | > 10) and (abs(pixel[0] - r) + abs(pixel[1] - g) + abs(pixel[2] - b) > 10): 289 | if left_value == j: 290 | left_count = left_count + 1 291 | else: 292 | left_value = j 293 | left_count = 1 294 | 295 | if left_count > 3: 296 | from_left_find_board_y = i - 3 297 | break 298 | # 逻辑跟上面类似,但是方向从右向左 299 | # 当有遮挡时,只会有一边有遮挡 300 | # 算出来两个必然有一个是对的 301 | for j in range(w)[::-1]: 302 | pixel = im_pixel[j, i] 303 | # 修掉脑袋比下一个小格子还高的情况的 bug 304 | if abs(j - piece_x) < piece_body_width: 305 | continue 306 | if (abs(pixel[0] - last_pixel[0]) + abs(pixel[1] - last_pixel[1]) + abs(pixel[2] - last_pixel[2]) 307 | > 10) and (abs(pixel[0] - r) + abs(pixel[1] - g) + abs(pixel[2] - b) > 10): 308 | if right_value == j: 309 | right_count = left_count + 1 310 | else: 311 | right_value = j 312 | right_count = 1 313 | 314 | if right_count > 3: 315 | from_right_find_board_y = i - 3 316 | break 317 | 318 | # 如果顶部像素比较多,说明图案近圆形,相应的求出来的值需要增大,这里暂定增大顶部宽的三分之一 319 | if board_x_c > 5: 320 | from_left_find_board_y = from_left_find_board_y + board_x_c / 3 321 | from_right_find_board_y = from_right_find_board_y + board_x_c / 3 322 | 323 | # 按实际的角度来算,找到接近下一个 board 中心的坐标 这里的角度应该是30°,值应该是tan 30°,math.sqrt(3) / 3 324 | board_y = piece_y - abs(board_x - piece_x) * math.sqrt(3) / 3 325 | 326 | # 从左从右取出两个数据进行对比,选出来更接近原来老算法的那个值 327 | if abs(board_y - from_left_find_board_y) > abs(from_right_find_board_y): 328 | new_board_y = from_right_find_board_y 329 | else: 330 | new_board_y = from_left_find_board_y 331 | 332 | if not all((board_x, board_y)): 333 | return 0, 0, 0, 0 334 | 335 | return piece_x, piece_y, board_x, new_board_y 336 | 337 | 338 | def check_screenshot(): 339 | ''' 340 | 检查获取截图的方式 341 | ''' 342 | global screenshot_way 343 | if os.path.isfile('autojump.png'): 344 | os.remove('autojump.png') 345 | if (screenshot_way < 0): 346 | print('暂不支持当前设备') 347 | sys.exit() 348 | pull_screenshot() 349 | try: 350 | Image.open('./autojump.png').load() 351 | print('采用方式 {} 获取截图'.format(screenshot_way)) 352 | except Exception: 353 | screenshot_way -= 1 354 | check_screenshot() 355 | 356 | 357 | def yes_or_no(prompt, true_value='y', false_value='n', default=True): 358 | default_value = true_value if default else false_value 359 | prompt = '%s %s/%s [%s]: ' % (prompt, true_value, false_value, default_value) 360 | i = input(prompt) 361 | if not i: 362 | return default 363 | while True: 364 | if i == true_value: 365 | return True 366 | elif i == false_value: 367 | return False 368 | prompt = 'Please input %s or %s: ' % (true_value, false_value) 369 | i = input(prompt) 370 | 371 | 372 | def main(): 373 | ''' 374 | 主函数 375 | ''' 376 | # op = yes_or_no('请确保手机打开了 ADB 并连接了电脑,然后打开跳一跳并【开始游戏】后再用本程序,确定开始?') 377 | # if not op: 378 | # print('bye') 379 | # return 380 | # 初始化AI 381 | ai.init() 382 | 383 | print('程序版本号:{}'.format(VERSION)) 384 | debug.dump_device_info() 385 | check_screenshot() 386 | 387 | i, next_rest, next_rest_time = 0, random.randrange(3, 10), random.randrange(5, 10) 388 | while True: 389 | pull_screenshot() 390 | im = Image.open('./autojump.png') 391 | # 获取棋子和 board 的位置 392 | piece_x, piece_y, board_x, board_y = find_piece_and_board(im) 393 | ts = int(time.time()) 394 | # print(ts, piece_x, piece_y, board_x, board_y) 395 | set_button_position(im) 396 | press_time = jump(math.sqrt((board_x - piece_x) ** 2 + (board_y - piece_y) ** 2)) 397 | 398 | # 在跳跃落下的瞬间 摄像机移动前截图 这个参数要自己校调 399 | time.sleep(0.2) 400 | pull_screenshot_temp() 401 | im_temp = Image.open('./autojump_temp.png') 402 | temp_piece_x, temp_piece_y = find_piece(im_temp) 403 | debug.computing_error(press_time, board_x, board_y, piece_x, piece_y, temp_piece_x, temp_piece_y) 404 | 405 | if debug_switch: 406 | debug.save_debug_screenshot(ts, im, piece_x, piece_y, board_x, board_y) 407 | debug.save_debug_screenshot(ts, im_temp, temp_piece_x, temp_piece_y, board_x, board_y) 408 | # debug.backup_screenshot(ts) 409 | i = 0 410 | if i == next_rest: 411 | print('已经连续打了 {} 下,休息 {}s'.format(i, next_rest_time)) 412 | for j in range(next_rest_time): 413 | sys.stdout.write('\r程序将在 {}s 后继续'.format(next_rest_time - j)) 414 | sys.stdout.flush() 415 | time.sleep(1) 416 | print('\n继续') 417 | i, next_rest, next_rest_time = 0, random.randrange(30, 100), random.randrange(10, 60) 418 | time.sleep(random.uniform(0.5, 0.6)) # 为了保证截图的时候应落稳了,多延迟一会儿,随机值防 ban 419 | 420 | 421 | if __name__ == '__main__': 422 | try: 423 | main() 424 | except KeyboardInterrupt: 425 | adb.run('kill-server') 426 | print('bye') 427 | exit(0) 428 | -------------------------------------------------------------------------------- /wechat_jump_auto_curves.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | ##基于python3.5(64位) 5 | ###如果缺少scikit-image库,建议进下面网址下载whl直接安装 6 | ##https://www.lfd.uci.edu/~gohlke/pythonlibs/#scikit-image 7 | 8 | 9 | === 思路 === 10 | 核心:每次落稳之后截图,根据截图算出棋子的坐标和下一个块顶面的中点坐标, 11 | 根据两个点的距离乘以一个时间系数获得长按的时间 12 | 识别棋子:靠棋子的颜色来识别位置,通过截图发现最下面一行大概是一条 13 | 直线,就从上往下一行一行遍历,比较颜色(颜色用了一个区间来比较) 14 | 找到最下面的那一行的所有点,然后求个中点,求好之后再让 Y 轴坐标 15 | 减小棋子底盘的一半高度从而得到中心点的坐标 16 | 识别棋盘:靠底色和方块的色差来做,从分数之下的位置开始,一行一行扫描, 17 | 由于圆形的块最顶上是一条线,方形的上面大概是一个点,所以就 18 | 用类似识别棋子的做法多识别了几个点求中点,这时候得到了块中点的 X 19 | 轴坐标,这时候假设现在棋子在当前块的中心,根据一个通过截图获取的 20 | 固定的角度来推出中点的 Y 坐标 21 | 最后:根据两点的坐标算距离乘以系数来获取长按时间(似乎可以直接用 X 轴距离) 22 | """ 23 | from __future__ import print_function, division 24 | import sys 25 | import time 26 | import math 27 | import random 28 | from PIL import Image 29 | from six.moves import input 30 | from skimage import io,transform 31 | import numpy as np 32 | import tensorflow as tf 33 | 34 | try: 35 | from common import debug, config, screenshot, UnicodeStreamFilter 36 | from common.auto_adb import auto_adb 37 | except Exception as ex: 38 | print(ex) 39 | print('请将脚本放在项目根目录中运行') 40 | print('请检查项目根目录中的 common 文件夹是否存在') 41 | exit(1) 42 | adb = auto_adb() 43 | VERSION = "1.1.4" 44 | 45 | # DEBUG 开关,需要调试的时候请改为 True,不需要调试的时候为 False 46 | DEBUG_SWITCH = False 47 | 48 | 49 | # Magic Number,不设置可能无法正常执行,请根据具体截图从上到下按需 50 | # 设置,设置保存在 config 文件夹中 51 | config = config.open_accordant_config() 52 | under_game_score_y = config['under_game_score_y'] 53 | # 长按的时间系数,请自己根据实际情况调节 54 | press_coefficient = config['press_coefficient'] 55 | # 二分之一的棋子底座高度,可能要调节 56 | piece_base_height_1_2 = config['piece_base_height_1_2'] 57 | # 棋子的宽度,比截图中量到的稍微大一点比较安全,可能要调节 58 | piece_body_width = config['piece_body_width'] 59 | 60 | target_score=1024 ##目标分数 61 | total_step=30 ##达到目标次数所需游戏次数 62 | start_score=100 ##设置第一次分数(目前分数) 63 | 64 | 65 | 66 | def set_button_position(im): 67 | """ 68 | 将 swipe 设置为 `再来一局` 按钮的位置 69 | """ 70 | global swipe_x1, swipe_y1, swipe_x2, swipe_y2 71 | w, h = im.size 72 | left = int(w / 2) 73 | top = int(1584 * (h / 1920.0)) 74 | left = int(random.uniform(left - 100, left + 100)) 75 | top = int(random.uniform(top - 100, top + 100)) # 随机防 ban 76 | after_top = int(random.uniform(top - 100, top + 100)) 77 | after_left = int(random.uniform(left - 100, left + 100)) 78 | swipe_x1, swipe_y1, swipe_x2, swipe_y2 = left, top, after_left, after_top 79 | 80 | 81 | 82 | def jump(distance): 83 | """ 84 | 跳跃一定的距离 85 | """ 86 | press_time = distance * press_coefficient 87 | press_time = max(press_time, 200) # 设置 200ms 是最小的按压时间 88 | press_time = int(press_time) 89 | 90 | cmd = 'shell input swipe {x1} {y1} {x2} {y2} {duration}'.format( 91 | x1=swipe_x1, 92 | y1=swipe_y1, 93 | x2=swipe_x2, 94 | y2=swipe_y2, 95 | duration=press_time 96 | ) 97 | print('{} {}'.format(adb.adb_path, cmd)) 98 | adb.run(cmd) 99 | return press_time 100 | 101 | 102 | def find_piece_and_board(im): 103 | """ 104 | 寻找关键坐标 105 | """ 106 | w, h = im.size 107 | 108 | piece_x_sum = 0 109 | piece_x_c = 0 110 | piece_y_max = 0 111 | board_x = 0 112 | board_y = 0 113 | scan_x_border = int(w / 8) # 扫描棋子时的左右边界 114 | scan_start_y = 0 # 扫描的起始 y 坐标 115 | im_pixel = im.load() 116 | # 以 50px 步长,尝试探测 scan_start_y 117 | for i in range(int(h / 3), int(h*2 / 3), 50): 118 | last_pixel = im_pixel[0, i] 119 | for j in range(1, w): 120 | pixel = im_pixel[j, i] 121 | # 不是纯色的线,则记录 scan_start_y 的值,准备跳出循环 122 | if pixel != last_pixel: 123 | scan_start_y = i - 50 124 | break 125 | if scan_start_y: 126 | break 127 | print('scan_start_y: {}'.format(scan_start_y)) 128 | 129 | # 从 scan_start_y 开始往下扫描,棋子应位于屏幕上半部分,这里暂定不超过 2/3 130 | for i in range(scan_start_y, int(h * 2 / 3)): 131 | # 横坐标方面也减少了一部分扫描开销 132 | for j in range(scan_x_border, w - scan_x_border): 133 | pixel = im_pixel[j, i] 134 | # 根据棋子的最低行的颜色判断,找最后一行那些点的平均值,这个颜 135 | # 色这样应该 OK,暂时不提出来 136 | if (50 < pixel[0] < 60) \ 137 | and (53 < pixel[1] < 63) \ 138 | and (95 < pixel[2] < 110): 139 | piece_x_sum += j 140 | piece_x_c += 1 141 | piece_y_max = max(i, piece_y_max) 142 | 143 | if not all((piece_x_sum, piece_x_c)): 144 | return 0, 0, 0, 0 145 | piece_x = int(piece_x_sum / piece_x_c) 146 | piece_y = piece_y_max - piece_base_height_1_2 # 上移棋子底盘高度的一半 147 | 148 | # 限制棋盘扫描的横坐标,避免音符 bug 149 | if piece_x < w/2: 150 | board_x_start = piece_x 151 | board_x_end = w 152 | else: 153 | board_x_start = 0 154 | board_x_end = piece_x 155 | 156 | for i in range(int(h / 3), int(h * 2 / 3)): 157 | last_pixel = im_pixel[0, i] 158 | if board_x or board_y: 159 | break 160 | board_x_sum = 0 161 | board_x_c = 0 162 | 163 | for j in range(int(board_x_start), int(board_x_end)): 164 | pixel = im_pixel[j, i] 165 | # 修掉脑袋比下一个小格子还高的情况的 bug 166 | if abs(j - piece_x) < piece_body_width: 167 | continue 168 | 169 | # 修掉圆顶的时候一条线导致的小 bug,这个颜色判断应该 OK,暂时不提出来 170 | if abs(pixel[0] - last_pixel[0]) \ 171 | + abs(pixel[1] - last_pixel[1]) \ 172 | + abs(pixel[2] - last_pixel[2]) > 10: 173 | board_x_sum += j 174 | board_x_c += 1 175 | if board_x_sum: 176 | board_x = board_x_sum / board_x_c 177 | last_pixel = im_pixel[board_x, i] 178 | 179 | # 从上顶点往下 +274 的位置开始向上找颜色与上顶点一样的点,为下顶点 180 | # 该方法对所有纯色平面和部分非纯色平面有效,对高尔夫草坪面、木纹桌面、 181 | # 药瓶和非菱形的碟机(好像是)会判断错误 182 | for k in range(i+274, i, -1): # 274 取开局时最大的方块的上下顶点距离 183 | pixel = im_pixel[board_x, k] 184 | if abs(pixel[0] - last_pixel[0]) \ 185 | + abs(pixel[1] - last_pixel[1]) \ 186 | + abs(pixel[2] - last_pixel[2]) < 10: 187 | break 188 | board_y = int((i+k) / 2) 189 | 190 | # 如果上一跳命中中间,则下个目标中心会出现 r245 g245 b245 的点,利用这个 191 | # 属性弥补上一段代码可能存在的判断错误 192 | # 若上一跳由于某种原因没有跳到正中间,而下一跳恰好有无法正确识别花纹,则有 193 | # 可能游戏失败,由于花纹面积通常比较大,失败概率较低 194 | for j in range(i, i+200): 195 | pixel = im_pixel[board_x, j] 196 | if abs(pixel[0] - 245) + abs(pixel[1] - 245) + abs(pixel[2] - 245) == 0: 197 | board_y = j + 10 198 | break 199 | 200 | if not all((board_x, board_y)): 201 | return 0, 0, 0, 0 202 | return piece_x, piece_y, board_x, board_y 203 | 204 | 205 | def yes_or_no(prompt, true_value='y', false_value='n', default=True): 206 | """ 207 | 检查是否已经为启动程序做好了准备 208 | """ 209 | default_value = true_value if default else false_value 210 | prompt = '{} {}/{} [{}]: '.format(prompt, true_value, 211 | false_value, default_value) 212 | i = input(prompt) 213 | if not i: 214 | return default 215 | while True: 216 | if i == true_value: 217 | return True 218 | elif i == false_value: 219 | return False 220 | prompt = 'Please input {} or {}: '.format(true_value, false_value) 221 | i = input(prompt) 222 | 223 | def pross_data(image): 224 | pixels = list(image.getdata()) # 得到像素数据 灰度0-255 225 | #print(len(pixels)) 226 | for i in range(len(pixels)): 227 | if pixels[i]<100: 228 | pixels[i]=0 229 | else: 230 | pixels[i]=255 231 | return pixels 232 | 233 | def pixel_division(img,w,h): 234 | pixels = list(img.getdata()) 235 | row_pix=np.zeros([1,h]) 236 | col_pix=np.zeros([1,w]) 237 | for i in range(w): 238 | for j in range(h): 239 | if pixels[j*w+i]<100: 240 | row_pix[0,j]+=1 241 | col_pix[0,i]+=1 242 | start_h=0 243 | end_h=0 244 | flag=0 245 | for j in range(h): 246 | if row_pix[0,j]>=1 and flag==0: 247 | start_h=j 248 | flag=1 249 | if row_pix[0,j]>=1: 250 | end_h=j 251 | 252 | pixels_Widh=[] 253 | end_w=0 254 | for i in range(1,w): 255 | if col_pix[0,i-1]<=0 and col_pix[0,i]>=1: 256 | pixels_Widh.append(i-1) 257 | if col_pix[0,i]>=1: 258 | end_w=i 259 | pixels_Widh.append(end_w+1) 260 | return start_h,end_h,pixels_Widh 261 | 262 | def strint(score0): 263 | if(score0<10): 264 | return str(score0) 265 | else: 266 | return "" 267 | 268 | def read_one_image(path): 269 | img = io.imread(path) 270 | w=81 271 | h=81 272 | c=1 273 | img = transform.resize(img,(w,h,c)) 274 | return np.asarray(img) 275 | 276 | def main(): 277 | """ 278 | 主函数 279 | """ 280 | op = yes_or_no('请确保手机打开了 ADB 并连接了电脑,' 281 | '然后打开跳一跳并【开始游戏】后再用本程序,确定开始?') 282 | if not op: 283 | print('bye') 284 | return 285 | print('程序版本号:{}'.format(VERSION)) 286 | debug.dump_device_info() 287 | screenshot.check_screenshot() 288 | 289 | i, next_rest, next_rest_time = (0, random.randrange(3, 10), 290 | random.randrange(5, 10)) 291 | j= 0 292 | ################ 分数曲线公式 293 | 294 | y_score=[] 295 | next_start=0 296 | global start_score 297 | for i in range(total_step): 298 | each_score=target_score*(1-np.exp(-0.15*(1024.0/target_score)*i)) 299 | y_score.append(each_score) 300 | if start_score>each_score: 301 | next_start=i 302 | next_start+=1 303 | #print(y_score) 304 | if start_score y_score[next_start]: ##自动结束这一次 355 | print("----------------") 356 | jump(math.sqrt((board_x - piece_x) ** 2 + (board_y - piece_y) ** 2)*5) 357 | next_start+=1 358 | time.sleep(5*random.random()) 359 | if next_start >len(y_score): 360 | break 361 | jump(math.sqrt((board_x - piece_x) ** 2 + (board_y - piece_y) ** 2)) 362 | if DEBUG_SWITCH: 363 | debug.save_debug_screenshot(ts, im, piece_x, 364 | piece_y, board_x, board_y) 365 | debug.backup_screenshot(ts) 366 | im.close() 367 | i += 1 368 | j += 1 369 | if i == next_rest: 370 | print('已经连续打了 {} 下,休息 {}s'.format(i, next_rest_time)) 371 | for j in range(next_rest_time): 372 | sys.stdout.write('\r程序将在 {}s 后继续'.format(next_rest_time - j)) 373 | sys.stdout.flush() 374 | time.sleep(1) 375 | print('\n继续') 376 | i, next_rest, next_rest_time = (0, random.randrange(30, 100), 377 | random.randrange(10, 60)) 378 | # 为了保证截图的时候应落稳了,多延迟一会儿,随机值防 ban 379 | time.sleep(random.uniform(0.9, 1.2)) 380 | 381 | 382 | if __name__ == '__main__': 383 | try: 384 | main() 385 | except KeyboardInterrupt: 386 | adb.run('kill-server') 387 | print('bye') 388 | exit(0) 389 | -------------------------------------------------------------------------------- /wechat_jump_auto_iOS.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | # === 思路 === 5 | # 核心:每次落稳之后截图,根据截图算出棋子的坐标和下一个块顶面的中点坐标, 6 | # 根据两个点的距离乘以一个时间系数获得长按的时间 7 | # 识别棋子:靠棋子的颜色来识别位置,通过截图发现最下面一行大概是一条 8 | 直线,就从上往下一行一行遍历,比较颜色(颜色用了一个区间来比较) 9 | 找到最下面的那一行的所有点,然后求个中点,求好之后再让 Y 轴坐标 10 | 减小棋子底盘的一半高度从而得到中心点的坐标 11 | # 识别棋盘:靠底色和方块的色差来做,从分数之下的位置开始,一行一行扫描, 12 | 由于圆形的块最顶上是一条线,方形的上面大概是一个点,所以就 13 | 用类似识别棋子的做法多识别了几个点求中点,这时候得到了块中点的 X 14 | 轴坐标,这时候假设现在棋子在当前块的中心,根据一个通过截图获取的 15 | 固定的角度来推出中点的 Y 坐标 16 | # 最后:根据两点的坐标算距离乘以系数来获取长按时间(似乎可以直接用 X 轴距离) 17 | """ 18 | import os 19 | import shutil 20 | import time 21 | import math 22 | import random 23 | import json 24 | from PIL import Image, ImageDraw 25 | import wda 26 | 27 | 28 | with open('config.json', 'r') as f: 29 | config = json.load(f) 30 | 31 | 32 | # Magic Number,不设置可能无法正常执行,请根据具体截图从上到下按需设置 33 | under_game_score_y = config['under_game_score_y'] 34 | # 长按的时间系数,请自己根据实际情况调节 35 | press_coefficient = config['press_coefficient'] 36 | # 二分之一的棋子底座高度,可能要调节 37 | piece_base_height_1_2 = config['piece_base_height_1_2'] 38 | # 棋子的宽度,比截图中量到的稍微大一点比较安全,可能要调节 39 | piece_body_width = config['piece_body_width'] 40 | time_coefficient = config['press_coefficient'] 41 | 42 | # 模拟按压的起始点坐标,需要自动重复游戏请设置成“再来一局”的坐标 43 | swipe = config.get('swipe', { 44 | "x1": 320, 45 | "y1": 410, 46 | "x2": 320, 47 | "y2": 410 48 | }) 49 | VERSION = "1.1.4" 50 | c = wda.Client() 51 | s = c.session() 52 | 53 | screenshot_backup_dir = 'screenshot_backups/' 54 | if not os.path.isdir(screenshot_backup_dir): 55 | os.mkdir(screenshot_backup_dir) 56 | 57 | 58 | def pull_screenshot(): 59 | c.screenshot('1.png') 60 | 61 | 62 | def jump(distance): 63 | press_time = distance * time_coefficient / 1000 64 | print('press time: {}'.format(press_time)) 65 | s.tap_hold(random.uniform(0, 320), random.uniform(64, 320), press_time) 66 | 67 | 68 | def backup_screenshot(ts): 69 | """ 70 | 为了方便失败的时候 debug 71 | """ 72 | if not os.path.isdir(screenshot_backup_dir): 73 | os.mkdir(screenshot_backup_dir) 74 | shutil.copy('1.png', '{}{}.png'.format(screenshot_backup_dir, ts)) 75 | 76 | 77 | def save_debug_creenshot(ts, im, piece_x, piece_y, board_x, board_y): 78 | draw = ImageDraw.Draw(im) 79 | # 对debug图片加上详细的注释 80 | draw.line((piece_x, piece_y) + (board_x, board_y), fill=2, width=3) 81 | draw.line((piece_x, 0, piece_x, im.size[1]), fill=(255, 0, 0)) 82 | draw.line((0, piece_y, im.size[0], piece_y), fill=(255, 0, 0)) 83 | draw.line((board_x, 0, board_x, im.size[1]), fill=(0, 0, 255)) 84 | draw.line((0, board_y, im.size[0], board_y), fill=(0, 0, 255)) 85 | draw.ellipse( 86 | (piece_x - 10, piece_y - 10, piece_x + 10, piece_y + 10), 87 | fill=(255, 0, 0)) 88 | draw.ellipse( 89 | (board_x - 10, board_y - 10, board_x + 10, board_y + 10), 90 | fill=(0, 0, 255)) 91 | del draw 92 | im.save('{}{}_d.png'.format(screenshot_backup_dir, ts)) 93 | 94 | 95 | def set_button_position(im): 96 | """ 97 | 将swipe设置为 `再来一局` 按钮的位置 98 | """ 99 | global swipe_x1, swipe_y1, swipe_x2, swipe_y2 100 | w, h = im.size 101 | left = w / 2 102 | top = 1003 * (h / 1280.0) + 10 103 | swipe_x1, swipe_y1, swipe_x2, swipe_y2 = left, top, left, top 104 | 105 | 106 | def find_piece_and_board(im): 107 | w, h = im.size 108 | 109 | print("size: {}, {}".format(w, h)) 110 | 111 | piece_x_sum = piece_x_c = piece_y_max = 0 112 | board_x = board_y = 0 113 | scan_x_border = int(w / 8) # 扫描棋子时的左右边界 114 | scan_start_y = 0 # 扫描的起始 y 坐标 115 | im_pixel = im.load() 116 | 117 | # 以 50px 步长,尝试探测 scan_start_y 118 | for i in range(under_game_score_y, h, 50): 119 | last_pixel = im_pixel[0, i] 120 | for j in range(1, w): 121 | pixel = im_pixel[j, i] 122 | 123 | # 不是纯色的线,则记录scan_start_y的值,准备跳出循环 124 | if pixel != last_pixel: 125 | scan_start_y = i - 50 126 | break 127 | 128 | if scan_start_y: 129 | break 130 | 131 | print("scan_start_y: ", scan_start_y) 132 | 133 | # 从 scan_start_y 开始往下扫描,棋子应位于屏幕上半部分,这里暂定不超过 2/3 134 | for i in range(scan_start_y, int(h * 2 / 3)): 135 | # 横坐标方面也减少了一部分扫描开销 136 | for j in range(scan_x_border, w - scan_x_border): 137 | pixel = im_pixel[j, i] 138 | # 根据棋子的最低行的颜色判断,找最后一行那些点的平均值,这个颜 139 | # 色这样应该 OK,暂时不提出来 140 | if (50 < pixel[0] < 60) \ 141 | and (53 < pixel[1] < 63) \ 142 | and (95 < pixel[2] < 110): 143 | piece_x_sum += j 144 | piece_x_c += 1 145 | piece_y_max = max(i, piece_y_max) 146 | 147 | if not all((piece_x_sum, piece_x_c)): 148 | return 0, 0, 0, 0 149 | piece_x = piece_x_sum / piece_x_c 150 | piece_y = piece_y_max - piece_base_height_1_2 # 上移棋子底盘高度的一半 151 | 152 | for i in range(int(h / 3), int(h * 2 / 3)): 153 | last_pixel = im_pixel[0, i] 154 | if board_x or board_y: 155 | break 156 | board_x_sum = 0 157 | board_x_c = 0 158 | 159 | for j in range(w): 160 | pixel = im_pixel[j, i] 161 | # 修掉脑袋比下一个小格子还高的情况的 bug 162 | if abs(j - piece_x) < piece_body_width: 163 | continue 164 | 165 | # 修掉圆顶的时候一条线导致的小 bug,这个颜色判断应该 OK,暂时不提出来 166 | if abs(pixel[0] - last_pixel[0]) \ 167 | + abs(pixel[1] - last_pixel[1]) \ 168 | + abs(pixel[2] - last_pixel[2]) > 10: 169 | board_x_sum += j 170 | board_x_c += 1 171 | 172 | if board_x_sum: 173 | board_x = board_x_sum / board_x_c 174 | 175 | # 按实际的角度来算,找到接近下一个 board 中心的坐标 这里的角度应该 176 | # 是 30°,值应该是 tan 30°, math.sqrt(3) / 3 177 | board_y = piece_y - abs(board_x - piece_x) * math.sqrt(3) / 3 178 | 179 | if not all((board_x, board_y)): 180 | return 0, 0, 0, 0 181 | 182 | return piece_x, piece_y, board_x, board_y 183 | 184 | 185 | def main(): 186 | while True: 187 | pull_screenshot() 188 | im = Image.open("./1.png") 189 | 190 | # 获取棋子和 board 的位置 191 | piece_x, piece_y, board_x, board_y = find_piece_and_board(im) 192 | ts = int(time.time()) 193 | print(ts, piece_x, piece_y, board_x, board_y) 194 | if piece_x == 0: 195 | return 196 | 197 | set_button_position(im) 198 | distance = math.sqrt( 199 | (board_x - piece_x) ** 2 + (board_y - piece_y) ** 2) 200 | jump(distance) 201 | 202 | save_debug_creenshot(ts, im, piece_x, piece_y, board_x, board_y) 203 | backup_screenshot(ts) 204 | # 为了保证截图的时候应落稳了,多延迟一会儿,随机值防 ban 205 | time.sleep(random.uniform(1, 1.1)) 206 | 207 | 208 | if __name__ == '__main__': 209 | main() 210 | -------------------------------------------------------------------------------- /wechat_jump_auto_slim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | __author__ = 'Erimus' 4 | ''' 5 | 这个是精简版本,只取x轴距离。 6 | 可以适配任意屏幕。 7 | 把磁盘读写截图改为内存读写。 8 | 可以防止被ban(从抓包数据看没有返回Error)。 9 | ''' 10 | 11 | import os 12 | import sys 13 | import subprocess 14 | import time 15 | import random 16 | from PIL import Image, ImageDraw 17 | from io import BytesIO 18 | 19 | # ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ 20 | VERSION = "1.1.4" 21 | screenshot_way = 2 22 | 23 | 24 | def check_screenshot(): # 检查获取截图的方式 25 | global screenshot_way 26 | if (screenshot_way < 0): 27 | print('暂不支持当前设备') 28 | sys.exit() 29 | binary_screenshot = pull_screenshot() 30 | try: 31 | Image.open(BytesIO(binary_screenshot)).load() # 直接使用内存IO 32 | print('Capture Method: {}'.format(screenshot_way)) 33 | except Exception: 34 | screenshot_way -= 1 35 | check_screenshot() 36 | 37 | 38 | def pull_screenshot(): # 获取截图 39 | global screenshot_way 40 | if screenshot_way in [1, 2]: 41 | process = subprocess.Popen( 42 | 'adb shell screencap -p', shell=True, stdout=subprocess.PIPE) 43 | screenshot = process.stdout.read() 44 | if screenshot_way == 2: 45 | binary_screenshot = screenshot.replace(b'\r\n', b'\n') 46 | else: 47 | binary_screenshot = screenshot.replace(b'\r\r\n', b'\n') 48 | return binary_screenshot 49 | elif screenshot_way == 0: 50 | os.system('adb shell screencap -p /sdcard/autojump.png') 51 | os.system('adb pull /sdcard/autojump.png .') 52 | 53 | # ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ 54 | 55 | 56 | def find_piece_and_board(im): # 寻找起点和终点坐标 57 | w, h = im.size # 图片宽高 58 | im_pixel = im.load() 59 | 60 | def find_piece(pixel): # 棋子取色精确范围 61 | return ((40 < pixel[0] < 65) and 62 | (40 < pixel[1] < 65) and 63 | (80 < pixel[2] < 105)) 64 | 65 | # 寻找棋子 ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ 66 | 67 | # 粗查棋子位置 68 | piece_found, piece_fx, piece_fy = 0, 0, 0 69 | scan_piece_unit = w // 40 # 间隔单位 70 | ny = (h + w) // 2 # 寻找下限 从画面中央的正方形的下缘开始 71 | while ny > (h - w) // 2 and not piece_found: 72 | ny -= scan_piece_unit 73 | for nx in range(0, w, scan_piece_unit): 74 | pixel = im_pixel[nx, ny] 75 | if find_piece(pixel): 76 | piece_fx, piece_fy = nx, ny 77 | piece_found = True 78 | break 79 | print('%-12s %s,%s' % ('piece_fuzzy:', piece_fx, piece_fy)) 80 | if not piece_fx: 81 | return 0, 0 # 没找到棋子 82 | 83 | # 精查棋子位置 84 | piece_x, piece_x_set = 0, [] # 棋子x/棋子坐标集合 85 | piece_width = w // 14 # 估算棋子宽度 86 | piece_height = w // 5 # 估算棋子高度 87 | for ny in range(piece_fy + scan_piece_unit, piece_fy - piece_height, -4): 88 | for nx in range(max(piece_fx - piece_width, 0), 89 | min(piece_fx + piece_width, w)): 90 | pixel = im_pixel[nx, ny] 91 | # print(nx,ny,pixel) 92 | if find_piece(pixel): 93 | piece_x_set.append(nx) 94 | if len(piece_x_set) > 10: 95 | piece_x = sum(piece_x_set) / len(piece_x_set) 96 | break 97 | print('%-12s %s' % ('p_exact_x:', piece_x)) 98 | 99 | # 寻找落点 ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ 100 | board_x = 0 101 | # 限制棋盘扫描的横坐标 避免音符bug 102 | if piece_x < w / 2: 103 | board_x_start, board_x_end = w // 2, w # 起点和终点的中点是画面中心 104 | else: 105 | board_x_start, board_x_end = 0, w // 2 106 | 107 | # 寻找落点顶点 108 | board_x_set = [] # 目标坐标集合/改为list避免去重 109 | for by in range((h - w) // 2, (h + w) // 2, 4): 110 | bg_pixel = im_pixel[0, by] 111 | for bx in range(board_x_start, board_x_end): 112 | pixel = im_pixel[bx, by] 113 | # 修掉脑袋比下一个小格子还高的情况 屏蔽小人左右的范围 114 | if abs(bx - piece_x) < piece_width: 115 | continue 116 | 117 | # 修掉圆顶的时候一条线导致的小bug 这个颜色判断应该OK 118 | if (abs(pixel[0] - bg_pixel[0]) + 119 | abs(pixel[1] - bg_pixel[1]) + 120 | abs(pixel[2] - bg_pixel[2]) > 10): 121 | board_x_set.append(bx) 122 | 123 | if len(board_x_set) > 10: 124 | board_x = sum(board_x_set) / len(board_x_set) 125 | print('%-12s %s' % ('target_x:', board_x)) 126 | break # 找到了退出 127 | 128 | return piece_x, board_x 129 | 130 | # ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ 131 | 132 | 133 | def set_button_position(im, gameover=0): # 重设点击位置 再来一局位置 134 | w, h = im.size 135 | if h // 16 > w // 9 + 2: # 长窄屏 2px容差 获取ui描绘的高度 136 | uih = int(w / 9 * 16) 137 | else: 138 | uih = h 139 | # uiw = int(uih / 16 * 9) 140 | 141 | # 如果游戏结束 点击再来一局 142 | left = int(w / 2) # 按钮半宽约uiw//5 143 | # 根据9:16实测按钮高度中心0.825 按钮半高约uiw//28 144 | top = int((h - uih) / 2 + uih * 0.825) 145 | if gameover: 146 | return left, top 147 | 148 | # 游戏中点击 随机位置防 ban 149 | left = random.randint(w // 4, w - 20) # 避开左下角按钮 150 | top = random.randint(h * 3 // 4, h - 20) 151 | return left, top 152 | 153 | # ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ 154 | 155 | 156 | def jump(piece_x, board_x, im, swipe_x1, swipe_y1): 157 | distanceX = abs(board_x - piece_x) # 起点到目标的水平距离 158 | shortEdge = min(im.size) # 屏幕宽度 159 | jumpPercent = distanceX / shortEdge # 跳跃百分比 160 | jumpFullWidth = 1700 # 跳过整个宽度 需要按压的毫秒数 161 | press_time = round(jumpFullWidth * jumpPercent) # 按压时长 162 | press_time = 0 if not press_time else max( 163 | press_time, 200) # press_time大于0时限定最小值 164 | print('%-12s %.2f%% (%s/%s) | Press: %sms' % 165 | ('Distance:', jumpPercent * 100, distanceX, shortEdge, press_time)) 166 | 167 | cmd = 'adb shell input swipe {x1} {y1} {x2} {y2} {duration}'.format( 168 | x1=swipe_x1, 169 | y1=swipe_y1, 170 | x2=swipe_x1 + random.randint(-10, 10), # 模拟位移 171 | y2=swipe_y1 + random.randint(-10, 10), 172 | duration=press_time 173 | ) 174 | os.system(cmd) 175 | 176 | # ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ 177 | 178 | 179 | def main(): 180 | check_screenshot() # 检查截图 181 | 182 | count = 0 183 | while True: 184 | count += 1 185 | print('---\n%-12s %s (%s)' % ('Times:', count, int(time.time()))) 186 | 187 | # 获取截图 188 | binary_screenshot = pull_screenshot() 189 | im = Image.open(BytesIO(binary_screenshot)) 190 | w, h = im.size 191 | if w > h: 192 | im = im.rotate(-90, expand=True) # 添加图片方向判断 193 | # print('image | w:%s | h:%s'%(w,h)) 194 | 195 | # 获取棋子和 board 的位置 196 | piece_x, board_x = find_piece_and_board(im) 197 | gameover = 0 if all((piece_x, board_x)) else 1 198 | swipe_x1, swipe_y1 = set_button_position( 199 | im, gameover=gameover) # 随机点击位置 200 | 201 | # 标注截图并显示 202 | # draw = ImageDraw.Draw(im) 203 | # draw.line([piece_x, 0, piece_x, h], fill='blue', width=1) # start 204 | # draw.line([board_x, 0, board_x, h], fill='red', width=1) # end 205 | # draw.ellipse([swipe_x1 - 16, swipe_y1 - 16, 206 | # swipe_x1 + 16, swipe_y1 + 16], fill='red') # click 207 | # im.show() 208 | 209 | jump(piece_x, board_x, im, swipe_x1, swipe_y1) 210 | 211 | wait = (random.random())**5 * 9 + 1 # 停1~9秒 指数越高平均间隔越短 212 | print('---\nWait %.3f s...' % wait) 213 | time.sleep(wait) 214 | print('Continue!') 215 | 216 | # ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ 217 | 218 | 219 | if __name__ == '__main__': 220 | try: 221 | main() 222 | except KeyboardInterrupt: 223 | os.system('adb kill-server') 224 | print('bye') 225 | exit(0) 226 | -------------------------------------------------------------------------------- /wechat_jump_iOS_py3.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import wda 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | import matplotlib.animation as animation 7 | from PIL import Image 8 | 9 | # 截图距离 * time_coefficient = 按键时长 10 | # time_coefficient: 11 | # iphonex: 0.00125 12 | # iphone6: 0.00196 13 | # iphone6s plus: 0.00120 14 | time_coefficient = 0.00120 15 | VERSION = "1.1.4" 16 | 17 | c = wda.Client() 18 | s = c.session() 19 | 20 | 21 | def pull_screenshot(): 22 | c.screenshot('autojump.png') 23 | 24 | 25 | def jump(distance): 26 | press_time = distance * time_coefficient 27 | press_time = press_time 28 | print('press_time = ',press_time) 29 | s.tap_hold(200, 200, press_time) 30 | 31 | 32 | fig = plt.figure() 33 | pull_screenshot() 34 | img = np.array(Image.open('autojump.png')) 35 | im = plt.imshow(img, animated=True) 36 | 37 | update = True 38 | click_count = 0 39 | cor = [] 40 | 41 | 42 | def update_data(): 43 | return np.array(Image.open('autojump.png')) 44 | 45 | 46 | def updatefig(*args): 47 | global update 48 | if update: 49 | time.sleep(1) 50 | pull_screenshot() 51 | im.set_array(update_data()) 52 | update = False 53 | return im, 54 | 55 | 56 | def on_click(event): 57 | global update 58 | global ix, iy 59 | global click_count 60 | global cor 61 | 62 | ix, iy = event.xdata, event.ydata 63 | coords = [(ix, iy)] 64 | print('now = ', coords) 65 | cor.append(coords) 66 | 67 | click_count += 1 68 | if click_count > 1: 69 | click_count = 0 70 | cor1 = cor.pop() 71 | cor2 = cor.pop() 72 | 73 | distance = (cor1[0][0] - cor2[0][0])**2 + (cor1[0][1] - cor2[0][1])**2 74 | distance = distance ** 0.5 75 | print('distance = ', distance) 76 | jump(distance) 77 | update = True 78 | 79 | 80 | fig.canvas.mpl_connect('button_press_event', on_click) 81 | ani = animation.FuncAnimation(fig, updatefig, interval=50, blit=True) 82 | plt.show() 83 | -------------------------------------------------------------------------------- /wechat_jump_py3.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import time 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | import matplotlib.animation as animation 7 | from PIL import Image 8 | 9 | VERSION = "1.1.4" 10 | def pull_screenshot(): 11 | os.system('adb shell screencap -p /sdcard/autojump.png') 12 | os.system('adb pull /sdcard/autojump.png .') 13 | 14 | 15 | def jump(distance): 16 | press_time = distance * 1.35 17 | press_time = int(press_time) 18 | cmd = 'adb shell input swipe 320 410 320 410 ' + str(press_time) 19 | print(cmd) 20 | os.system(cmd) 21 | 22 | 23 | fig = plt.figure() 24 | pull_screenshot() 25 | img = np.array(Image.open('autojump.png')) 26 | im = plt.imshow(img, animated=True) 27 | 28 | update = True 29 | click_count = 0 30 | cor = [] 31 | 32 | 33 | def update_data(): 34 | return np.array(Image.open('autojump.png')) 35 | 36 | 37 | def updatefig(*args): 38 | global update 39 | if update: 40 | time.sleep(1.5) 41 | pull_screenshot() 42 | im.set_array(update_data()) 43 | update = False 44 | return im, 45 | 46 | 47 | def on_click(event): 48 | global update 49 | global ix, iy 50 | global click_count 51 | global cor 52 | 53 | ix, iy = event.xdata, event.ydata 54 | coords = [(ix, iy)] 55 | print('now = ', coords) 56 | cor.append(coords) 57 | 58 | click_count += 1 59 | if click_count > 1: 60 | click_count = 0 61 | cor1 = cor.pop() 62 | cor2 = cor.pop() 63 | 64 | distance = (cor1[0][0] - cor2[0][0])**2 + (cor1[0][1] - cor2[0][1])**2 65 | distance = distance ** 0.5 66 | print('distance = ', distance) 67 | jump(distance) 68 | update = True 69 | 70 | 71 | fig.canvas.mpl_connect('button_press_event', on_click) 72 | ani = animation.FuncAnimation(fig, updatefig, interval=50, blit=True) 73 | plt.show() 74 | --------------------------------------------------------------------------------